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
orapplication/problem+xml
- there are no required fields, but some are suggested, namely:
type
,title
,status
,detail
andinstance
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