In the last two articles about CRUD and HTML (CODE Magazine, November/December 2015 and January/February 2016), you created a product information page to display a list of product data returned from a Web API. In addition, you built functionality to add, update, and delete products using the same Web API controller. In those two articles, there was a very basic exception handler function named handleException
. This function displays error information returned from the API in an alert dialog on your page, as shown in Figure 1.
In this article, I'll expand upon the IHttpActionResult
interface you learned about in the last article (CODE Magazine, January/February 2016). You'll learn to determine what kind of error is returned from your Web API call and display different messages based on those errors. You'll learn to how to return 500, 404, and 400 exceptions and how to handle them on your Web page.
Basic Exception Handling in the Web API
If you remember from the last article, the Web API 2 provides a new interface called IHttpActionResult
that you use as the return value for all your methods. This interface is built into the ApiController
class (from which your ProductContoller
class inherits) and defines helper methods to return the most common HTTP status codes such as a 202, 201, 400, 404, and 500. There are a few different methods built-in to the MVC Controller class that you assign to your IHttpActionResult
return value. These methods and the HTTP status code they return are shown in the following list.
- OK: 200
- Created: 201
- BadRequest: 400
- NotFound: 404
- InternalServerError: 500
Each method in your controller should return one of the codes listed above. Each of these methods implements a class derived from the interface IHttpActionResult
. When you return an OK from your method, a 200 HTTP status code is returned, which signifies that no error has occurred. The Created
method, used in the Post
method, returns a 201 which means that the method succeeded in creating a new resource. The other three return values all signify that an error has occurred, and thus call the function handleException
specified in the error parameter of your Ajax. A typical Ajax call is shown below.
function productList() {
$.ajax({
url: '/api/Product/',
type: 'GET',
dataType: 'json',
success: function (products) {
productListSuccess(products);
},
error: function (request, message, error) {
handleException(request, message, error);
}
});
}
The next snippet is the handleException
function from the January/February) article. This function extracts different properties from the request parameter and displays all of the values in an alert dialog. A newline-delimited string displays each value - Code, Text, and Message - on a separate line in the alert dialog. There's no distinction in this function between 400, 404, or 500 errors.
function handleException(request, message, error) {
var msg = "";
msg += "Code: " + request.status + "\n";
msg += "Text: " + request.statusText + "\n";
if (request.responseJSON != null) {
msg += "Message: " +
request.responseJSON.Message + "\n";
}
alert(msg);
}
BadRequest: 400 Error
A 400 error, BadRequest, is used when you have validation errors from data posted back by the user. You pass a ModelStateDictionary
object to the BadRequest
method and it converts that dictionary into JSON which, in turn, is passed back to your HTML page. Within the new handleException
function, you're going to write, parse the dictionary of validation errors, and display the messages to the user.
NotFound: 404 Error
A 404 error is generated by a Web server when a user requests a page, or other resource, that is not valid on that Web server. When writing your Web API, use a 404 to generate an error to inform the programmer calling your API that they have requested a piece of data that's not available in your data store. For example, they may pass the following request to your Get
method; api/Product/999. If there is no product with a primary key of 999 in your database, return a 404 error to the HTML page so the programmer can inform the user that they requested data that isn't available.
InternalServerError: 500 Error
I'm sure that you've seen the dreaded 500 error come back from your Web application at some point or another while developing your application. A 500 error is returned when there's an unhandled exception in your application. You should always handle all of your exceptions yourself, but you still might wish to pass back a 500 to the HTML page. If you do, I suggest that you supply as much information about why a 500 is being returned as you can. Don't just let the .NET Framework dump their generic, and often unhelpful, error message back to the consumer of your Web API.
Unhandled Exceptions
To illustrate what happens when you have an unhandled exception in your Web API, open the ProductController
and locate the Get
method that returns a list of all products. Simulate an unhandled exception by adding the following lines of code at the top of this method.
[HttpGet()]
public IHttpActionResult Get()
{
int x = 0;
int y = 10;
int z = y / x;
// ... Rest of the code is here
}
When you run the Web page and it tries to load all of the product data into the table on the Web page, a 500 error is sent back to the client. NOTE: You might need to turn off the “Break when an exception is User Unhandled” from the Debug > Exceptions menu prior to running this code.
Modify the handleException
function (Listing 1) on your Web page to handle a 500 exception. Add a switch...case statement to the function. The first case you add is 500. You'll add more cases later in this article. When a 500 exception is received, grab the ExceptionMessage
property from the responseJSON
property on the request parameter. The ExceptionMessage
property contains the message from the .NET Exception object. For the case of the code you added to the controller, the message is “Attempted to divide by zero,” as shown in Figure 1.
Listing 1: Add a case statement to the handleException function to display the 500 error.
function handleException(request, message, error) {
var msg = "";
switch (request.status) {
case 500:
// Display error message thrown from the server
msg = request.responseJSON.ExceptionMessage;
break;
default:
msg = "Status: " + request.status;
msg += "\n" + "Error Message: " +
request.statusText;
break;
}
alert(msg);
}
After running this code and seeing the error, be sure to remove the three lines that cause the error so you can move on and try out the rest of the error handling in this article.
Return InternalServerError for Handled Exceptions
Instead of letting the divide-by-error exception go unhandled, let's explicitly return a 500 exception using the InternalServerError
method. Open the ProductController.cs
file and locate the Get(int id)
method. Add the same three lines within a try...catch block, as shown in Listing 2, to simulate an error. Create two catch blocks: one to handle a DivideByZeroException
and one to handle a generic Exception object. By catching an explicit DivideByZeroException
object, you can pass back a more specific error message to the HTML page.
Listing 2: Return the IHttpActionResult InternalServerError() when you want to control the 500 error coming back from the Web API.
[HttpGet()]
public IHttpActionResult Get(int id)
{
IHttpActionResult ret;
List<Product> list = new List<Product>();
Product prod = new Product();
try {
// Simulate an error
int x = 0;
int y = 10;
int z = y / x;
list = CreateMockData();
prod = list.Find(p => p.ProductId == id);
if (prod == null) {
ret = NotFound();
}
else {
ret = Ok(prod);
}
}
catch (DivideByZeroException) {
// Set return value to 500
ret = InternalServerError(
new Exception("In the Product.Get(id) method
a divide by zero was detected.
Please check your parameters"));
}
catch (Exception ex) {
// Do some logging here
// Set return value to 500
ret = InternalServerError(ex);
}
return ret;
}
After writing this code, run the application and click on any of the Edit buttons in the HTML table. You should now see the custom message that you wrote appear in the alert dialog on the page. Once again, after you see the error appear and you're satisfied that it's working correctly, remove the three lines of code so you can move on with the rest of the article.
Handling Validation Errors with BadRequest
As you know, you can't trust user data. Even if you have jQuery validation on your page, you can't guarantee that the validation ran. Thus, you need to perform validation of your data once it's posted back to your Web API. There are many ways to validate your data once it's posted back. One of the most popular methods today is to use Data Annotations. If you're using the Entity Framework (EF), data annotations are added to the entity class automatically. I'm not going to cover data annotations in this article, as I assume that you already know how to use those. If you don't, there are many articles available on the usage of data annotations.
You may find cases where you just can't accomplish the validation you need using data annotations. In those cases, perform data validation using traditional C# code. Create a ModelStateDictionary
to hold your validation error messages, or add to the ModelStateDictionary
returned from the EF engine. Open the ProductController.cs
and add the following Using statement at the top of the file.
using System.Web.Http.ModelBinding;
You need to create an instance of a ModelStateDictionary
class to hold your validation messages. Create a private field, named ValidationMessages
, in the ProductController
class.
private ModelStateDictionary
ValidationMessages { get; set; }
Build a method, named Validate()
, to check the product data passed into your Web API from an HTML page. Add a couple of checks for business rules, as shown in the code snippet below. Yes, these simple business rules can be done with data annotations, but I want to show you how to add custom rules.
private bool Validate(Product product) {
ValidationMessages = new ModelStateDictionary();
if (string.IsNullOrWhiteSpace(product.ProductName)) {
ValidationMessages.AddModelError(
"ProductName",
"Product Name must be filled in.");
}
if (string.IsNullOrWhiteSpace(product.Url)) {
ValidationMessages.AddModelError("Url",
"URL must be filled in.");
}
return (ValidationMessages.Count == 0);
}
In the Add
method you wrote in the last article, you added the new product data to the collection of products. Modify the Add
method to call the Validate
method you just wrote to see if all of the business rules passed prior to adding the product to the list (see Listing 3).
Listing 3: Check business rules prior to adding a product to your database and your list.
private bool Add(Product product)
{
int newId = 0;
bool ret = false;
List<Product> list = new List<Product>();
// Validate Product Data from user
ret = Validate(product);
if (ret) {
list = CreateMockData();
// Get the last id
newId = list.Max(p => p.ProductId);
newId++;
product.ProductId = newId;
// Add to list
list.Add(product);
ret = true;
}
return ret;
}
Now that you have the Add
method calling the new Validate
method, modify the Post
method, as shown in Listing 4. If the Add
method returns a false
value, write code in the else statement to return a BadRequest
with the collection of validation messages generated by the Validate
method.
Listing 4: Send BadRequest back to client with any validation messages.
[HttpPost()]
public IHttpActionResult Post(Product product)
{
IHttpActionResult ret = null;
if (Add(product)) {
ret = Created<Product>(
Request.RequestUri +
product.ProductId.ToString(),
product);
}
else {
ret = BadRequest(ValidationMessages);
}
return ret;
}
The ValidationsMessages
property, which is a ModelStateDictionary
object, gets serialized as JSON when it's sent back to the front end. In your HTML page you'll need to extract the error messages. The easiest way is to create a function, named getModelStateErrors
, as shown in Listing 5.
Listing 5: Use JSON.parse to create a jQuery array of ModelState objects.
function getModelStateErrors(errorText) {
var response = null;
var errors = [];
// Convert the error text from ModelState
// into a JSON object
try {
response = JSON.parse(errorText);
}
catch (e) {
}
if (response != null) {
// Extract keys from the ModelState portion
for (var key in response.ModelState) {
// Create list of error messages to display
errors.push(response.ModelState[key]);
}
}
return errors;
}
This method calls JSON.parse
to convert the JSON returned from the Web API into a JSON object that contains an array in a property named ModelState
. You loop through the array and extract each error message from the object and add that message to a new array named errors. This array is returned from this function and will be used to display the errors on the Web page.
Open your Index.cshtml page and locate the handleException
function. Add a new Case statement to handle these validation errors. What you add to the handleException
function is shown in the next code snippet.
case 400:
// 'Bad Request' means we are throwing back
// model state errors
var errors = [];
errors = getModelStateErrors(request.responseText);
for (var i = 0; i < errors.length; i++) {
msg += errors[i] + "\n";
}
break;
In this Case statement, call the getModelStateErrors
function passing in the request.responseText
that contains the JSON string returned from the Web API. Loop through the array of errors returned, and place a new line character at the end of each one, and concatenate to the variable msg
. This msg
variable is then displayed in the alert.
In this article, I'm appending error messages together to display in an alert dialog box. Feel free to modify this code to display the messages in any fashion you want. For instance, you might choose to create a bulleted list in a Bootstrap well to which you add the error messages. Or, you might display them in a Bootstrap pop-up dialog box. You're only limited by your own imagination.
Handling Data Not Found
When working in a multi-user environment, it's possible for one user to delete a record, but another wants to view or update that record after it's already been deleted. In that case, you need to inform the second user that the record they were trying to locate could not be found. You do this using returning the 404 status code using the method NotFound
, as shown in Listing 6. This is the same Get(int id)
method shown earlier in this article, except that I've removed the catch block for the DivideByZeroException
. If you search for the value contained in the ID parameter in the product list and you don't find it, set the ret
return variable to the object returned from the NotFound
method.
Listing 6: Return a NotFound when data can't be located in your database
[HttpGet()]
public IHttpActionResult Get(int id)
{
IHttpActionResult ret;
List<Product> list = new List<Product>();
Product prod = new Product();
try {
list = CreateMockData();
prod = list.Find(p => p.ProductId == id);
if (prod == null) {
ret = NotFound();
}
else {
ret = Ok(prod);
}
}
catch (Exception ex) {
// Do some logging here, or whatever you want to do
// Set return value to 500
ret = InternalServerError(ex);
}
return ret;
}
Modify the handleException
function in your Index.cshtml page to handle this new 404 value, as shown in the code snippet below. Because you can't pass anything back from the NotFound
method in your Web API, you need to add your own message on the page to specify exactly what could not be found. In this case, you inform the user that the product they requested could not be found.
case 404:
// 'Not Found' means the data you are requesting
// cannot be found in the database
msg = "The Product you were requesting could not be found";
break;
Summary
In this article, you learned how to use the IHttpActionResult
methods implemented in the .NET Controller class to return a specific HTTP status exception to the Web page that called the API. Using HTTP status codes allows you to add a case statement in your JavaScript to provide unique error messages to your user. I also recommend that, on the server-side, you log exceptions prior to returning an error to the front end.