Good afternoon everyone, and welcome to the Exception Handling in ASP.NET Web API Tour here at Exception Not Found. My name is Matthew and I'll be your tour guide today! Step right this way, please. Did everybody have a pleasant morning? Excellent!

Climb on board our brand-new Web API Elevator, and we'll get this tour started. First, let me introduce my brilliant assistant Postman; he'll be working with me on this tour. Please hold your questions for the end of the tour; there will be plenty of time to get to them all. With that, onward to the first floor! All aboard!

Two elevator operators in a store in Japan usher guests into the elevator Lift Ladies by Turelio, used under license

Level 1 - HttpResponseException

First floor: method level exceptions, Request.CreateResponse, and women's fragrances, as I am sure you can tell by now given the looks on your faces.

Let's try to ignore the pungent scents wafting from the sales counters and imagine that we have the following action in our controller:

[Route("CheckId/{id}")]
[HttpGet]
public IHttpActionResult CheckId(int id)  
{
    if(id > 100)
    {
        throw new ArgumentOutOfRangeException();
    }
    return Ok(id);
}

Postman, can you show us what the response from this method looks like with a valid ID?

A Postman response from CheckId, showing a status code 200 OK

Thank you. As you can see, our return status code is 200 - OK. That's exactly what we expect, since 4 is a valid ID. What happens if we submit an invalid ID?

A Postman response from CheckId, showing a status code 500 - Internal Server Error

Now our status code is 500 - Internal Server Error. So what does that mean? To the consumer, this is the equivalent of saying "an error occurred" and not getting any further explanation. Imagine how frustrating that would be (a scenario which we've all encountered, I am sure).

Instead of just throwing a naked exception, let's return a message to the consumer with a better explanation of what happened. In our case, since we used an invalid ID, let's return 400 - Bad Request to better describe the error. We can do this by using the HttpResponseException class:

[Route("CheckId/{id}")]
[HttpGet]
public IHttpActionResult CheckId(int id)  
{
    if (id > 100)
    {
        var message = new HttpResponseMessage(HttpStatusCode.BadRequest)
        {
            Content = new StringContent("We cannot use IDs greater than 100.")
        };
        throw new HttpResponseException(message);
    }
    return Ok(id);
}

Now the response looks like this:

A Postman response, showing the status code 400 Bad Request

For the do-it-yourself people out there, you could also just build the request in the action itself:

[Route("HttpError")]
[HttpGet]
public HttpResponseMessage HttpError()  
{
    return Request.CreateResponse(HttpStatusCode.Forbidden, "You cannot access this method at this time.");
}

Those of you who prefer simple living should know that there are also some shortcut methods for commonly-used status codes:

[Route("Forbidden")]
[HttpGet]
public IHttpActionResult Forbidden()  
{
    return Forbidden();
}

[Route("OK")]
[HttpGet]
public IHttpActionResult OK()  
{
    return Ok();
}

[Route("NotFound")]
[HttpGet]
public IHttpActionResult NotFound()  
{
    return NotFound();
}

Now before any of the eagle-eyed perfume salespeople hustle over here and try to block us from leaving, hurry back on board and we'll scoot on up to the second floor.

Level 2 - Exception Filters

Second floor: exception filters, attributes, and kids clothing!

This floor deals with exceptions at a slightly higher level, if you catch my drift. HttpResponseException and the shortcut methods are great for handling action-level exceptions, but what if we wanted to deal with many exceptions that could be thrown by many different actions? Let's see another controller action.

[Route("ItemNotFound/{id}")]
[HttpPost]
[ItemNotFoundExceptionFilter]
public IHttpActionResult ItemNotFound(int id)  
{
    _service.ThrowItemNotFoundException();
    return Ok();
}

The ThrowItemNotFoundException() method is a custom Exception, defined and used like so:

public class CustomExceptionService  
{
    public void ThrowItemNotFoundException()
    {
        throw new ItemNotFoundException("This is a custom exception.");
    }
}

public class ItemNotFoundException : Exception  
{
    public ItemNotFoundException(string message) : base(message) { }
    public ItemNotFoundException(string message, Exception ex) : base(message, ex) { }
}

ItemNotFoundExceptionFilter inherits from ExceptionFilterAttribute:

public class ItemNotFoundExceptionFilterAttribute : ExceptionFilterAttribute  
{
    public override void OnException(HttpActionExecutedContext context)
    {
        if (context.Exception is ItemNotFoundException)
        {
            var resp = new HttpResponseMessage(HttpStatusCode.NotFound)
            {
                Content = new StringContent(context.Exception.Message),
                ReasonPhrase = "ItemNotFound"
            };
            throw new HttpResponseException(resp);
        }
    }
}

Exception Filters are used whenever a controller action throws an unhandled exception that is not an HttpResponseException. Since they are attributes, we can decorate both controllers and actions with them. In this case, if we call the ItemNotFound action, we should see an HttpResponseException item thrown. Postman, what does this response look like?

A Postman response, showing a status code of 404 Item Not Found

Notice the "ItemNotFound" phrase next to the 404 status code. That phrase is the ReasonPhrase we set in the OnException method.

If the second floor doesn't quite fulfill all your exception handling needs, slide back into our elevator and continue on with me to the third floor. Doors closing, watch your fingers please!

Level 3 - Logging

Third floor: logging and leggings!

On this floor, you can use the built-in ExceptionLogger class that logs any exception in the application. It looks like this:

public class UnhandledExceptionLogger : ExceptionLogger  
{
    public override void Log(ExceptionLoggerContext context)
    {
        var log = context.Exception.ToString();
        //Do whatever logging you need to do here.
    }
}

To use this class in our Web API service, we need to register it. In the WebApiConfig file (or whatever file you are using to configure the service) we need to replace the default ExceptionLogger which Web API automatically adds to the services with our own implementation:

config.Services.Replace(typeof(IExceptionLogger), new UnhandledExceptionLogger());  

Now, any time an unhandled exception is thrown anywhere in the service, the logger will catch it, and you can write whatever logging code you wish to use.

Ladies, please no trying on the leggings unless you are willing to pay for them. Everybody who would like to visit the top floor, please step back onto the elevator. It's getting a little crowded in here, what will all the exceptions we seem to be catching, so please, make room wherever possible and we'll all get there in one piece.

Level 4 - Exception Handlers

Top floor, last stop! Everybody out!

The last step in our exception handling pipeline is an Exception Handler. Exception Handlers are called after Exception Filters and Exception Loggers, and only if the exception has not already been handled. Here's our Exception Handler:

public class GlobalExceptionHandler : ExceptionHandler  
{
    public override void Handle(ExceptionHandlerContext context)
    {
        if (context.Exception is ArgumentNullException)
        {
            var result = new HttpResponseMessage(HttpStatusCode.BadRequest)
            {
                Content = new StringContent(context.Exception.Message),
                ReasonPhrase = "ArgumentNullException"
            };

            context.Result = new ArgumentNullResult(context.Request, result);
        }
        else
        {
            // Handle other exceptions, do other things
        }
    }

    public class ArgumentNullResult : IHttpActionResult
    {
        private HttpRequestMessage _request;
        private HttpResponseMessage _httpResponseMessage;


        public ArgumentNullResult(HttpRequestMessage request, HttpResponseMessage httpResponseMessage)
        {
            _request = request;
            _httpResponseMessage = httpResponseMessage;
        }

        public Task<HttpResponseMessage> ExecuteAsync(CancellationToken cancellationToken)
        {
            return Task.FromResult(_httpResponseMessage);
        }
    }
}

The handler, like the logger, must be registered in the Web API configuration. Note that we can only have one Exception Handler per application.

config.Services.Replace(typeof(IExceptionHandler), new GlobalExceptionHandler());  

Now, let's add a new controller action:

[Route("ArgumentNull/{id}")]
[HttpPost]
public IHttpActionResult ArgumentNull(int id)  
{
    _service.ThrowArgumentNullException();
    return Ok();
}

If Postman calls this action, the response looks like this:

A Postman response, showing the status 400 ArgumentNullExcepton

This (the 404 ArgumentNullException status code and message) is, once again, what we expect. Now we are returning more descriptive errors to our clients, so they have a better idea of what is wrong and how to fix it. That's the excellent customer service we provide!

Summary

Web API provides us a great deal of flexibility in terms of exception handling. To recap:

  • Use HttpResponseException or the shortcut methods to deal with unhandled exceptions at the action level.
  • Use Exception Filters to deal with particular unhandled exceptions on multiple actions and controllers.
  • Use ExceptionLogger to log any unhandled exception.
  • Use Exception Handlers (one per application) to deal with any unhandled exception application-wide.

Don't forget to check out the demonstration display; it's right over there, in the GitHub corner. Also, please give a big round of applause to Postman, my wonderful assistant (and go download it if you haven't already).

If you'd like to take the stairs down, they are just around the corner; those of you who are going down please stay to the right and those of you who decide to come back up please stay to the left (we've had a lot of bumping incidents in the stairwells as of late).

If anybody has any questions, now's the time to ask them. Use the comments below. If not, thanks for coming on the tour, and have a great rest of your day.

Happy Coding!