Alica's dev blog
Handling ASP.NET Core WebApi errors with ProblemDetails

How to return API errors in a nice way?

Problem

For numerous reasons, we should want to avoid using exceptions for control flow, mostly because of the semantics (error doesn’t always have to be exceptional) and performance degradation.

But what should we do then?

I already described using Result class for dealing with API errors, taking inspiration from how other languages do it.

However, this approach doesn’t carry any information about the error except the error code. It would be more convenient if it also contained an error message.

Add error message to the Result class

We can simply add ErrorMessage property to the Result class.

public class Result
{
    // this is old
    public bool IsSuccess { get; set; }

    // this is old
    public ErrorCode? ErrorCode { get; set; }

    // this is new
    public string ErrorMessage { get; set; }

    ...
}

We also add new static methods to the class. They take the message as a parameter (both for non-generic and generic version):

public static Result Failure(ErrorCode errorCode, string errorMessage)
{
    return new Result
    {
        IsSuccess = false,
        ErrorCode = errorCode,
        ErrorMessage = errorMessage
    };
}

The only thing that needs to be updated in request handlers is calling the Failure method with that extra parameter.

What about the controller?

Let’s look at the current version of BooksController (link to the source code). In case the mediator returned error result, we check for the error code and return an appropriate HTTP response.

public async Task<ActionResult<Book>> Get(int id)
{
    var result = await mediator.Send(new GetBookRequest { Id = id });

    if (result.IsSuccess)
    {
        var book = result.Body;
        return Ok(book);
    }

    if (result.ErrorCode == ErrorCode.NotFound)
        return NotFound();

    return BadRequest();
}

How do we return the actual error message? BadRequest, NotFound and other methods from ControllerBase only expect a request body as a parameter, but we don’t want to just put the error string to the response body. There must be a better way…

Solution

Fortunately, it already exists!

Look at how ASP.NET Core does it

Good news: If you use ASP.NET Core’s model validation, you have most probably seen the solution:

{
  "type": "https://tools.ietf.org/html/rfc7231#section-6.5.1",
  "title": "One or more validation errors occurred.",
  "status": 400,
  "traceId": "|3c71f25d-43c2f779d5355713.",
  "errors": {
    "Name": [
      "The Name field is required."
    ]
  }
}

This type of response is called ProblemDetails and it’s a standard format to carry machine-readable error information. It has is own RFC (RFC 7807) and the main points are:

  • content type is application/problem+json or application/problem+xml
  • there are no required fields, but some are suggested, namely: type, title, status, detail and instance

There is a ProblemDetails class in ASP.NET Core and it’s also used to construct the above mentioned response.

Let’s use it too!

Construct your own ProblemDetails object

There already is the Problem method in ControllerBase that returns a ProblemDetails object.

If we create our own overload in our AbstractControllerBase, we will keep the logic in one place and won’t have to add a lot of code to the controllers.

protected ObjectResult Problem(Result failedResult)
{
	switch(failedResult.ErrorCode)
    {
		case ErrorCode.NotFound:
			return Problem(title: "Not Found", statusCode: (int)HttpStatusCode.NotFound, detail: failedResult.ErrorMessage);

        // our error codes don't necessarily have to have 1:1 mapping to HTTP errors
		case ErrorCode.IDontLikeYou:
			return Problem(title: "I'm a teapot", statusCode: 418, detail: failedResult.ErrorMessage);

		default:
			return Problem(title: "Bad Request", statusCode: (int)HttpStatusCode.BadRequest, detail: failedResult.ErrorMessage);
	}
}

Actually, we only need to call the Problem method from the controller and everything gets handled behind the scenes.

public async Task<ActionResult<Book>> Get(int id)
{
    var result = await mediator.Send(new GetBookRequest { Id = id });

    if (result.IsSuccess)
    {
        var book = result.Body;
        return Ok(book);
    }

    return Problem(result);
}

Don’t forget about the exceptions

You can also used ProblemDetails to return information about exceptions. However, I will just refer you to an existing article which provides a great deal of information on that: Creating a custom ErrorHandlerMiddleware function.


Last modified on 2021-02-05