ASP.NET Core - Handling API errors smoothly

Every API needs to create a pattern to deal with errors. Everyone knows about it, and probably every back-end developer needs to implement some features to give nice feedback to front-end developers (as they want). API’s, in general, are using at least 4 different status codes: OK, BadRequest, NotFound and InternalServerError, and when it comes to asp.net core we need to format 4 ObjectResult objects to fulfill this necessity.

To centralize the client result format logic and run away from solutions If / Switch based, we can use this approach:

Create a BaseException class

You can use a default abstract exception to identify when some familiar error happened, working as a template of the error.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public abstract class BaseException : Exception
{
public List<ValidationError> Errors { get; } = new List<ValidationError>();

public BaseException(string field, string fieldMessage, ErrorResultType type)
{
this.Errors.Add(new ValidationError(field, fieldMessage, type));
}

public BaseException(List<ValidationError> errors) : base()
{
this.Errors = errors;
}

public abstract ObjectResult GetObjectResult(ValidationErrorResult errorResult);
}

Extends Base Exception to a derived class

This way you can specify the ObjectResult in the Exception type, removing the necessity of If / Switch in the global error filter.

1
2
3
4
5
6
7
8
9
10
11
public class BadRequestException : BaseException
{
public BadRequestException(string field, string fieldMessage, ErrorResultType type) : bas(field, fieldMessage, type) { }

public BadRequestException(List<ValidationError> errors) : base(errors) { }

public override ObjectResult GetObjectResult(ValidationErrorResult errorResult)
{
return new BadRequestObjectResult(errorResult);;
}
}

Create an HTTP Global Filter

Every type of result will be treated in the same way, which makes the error result more dynamic and extendable. You just need to create a new Type of Error derived from BaseException, returning the desirable ObjectResult and that is it, nothing more needs to change.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
public class HttpGlobalExceptionFilter : IExceptionFilter
{
private readonly IHostingEnvironment _env;

private readonly ILogger<HttpGlobalExceptionFilter> _logger;

public HttpGlobalExceptionFilter(IHostingEnvironment env, ILogger<HttpGlobalExceptionFilter> logger)
{
this._env = env;
this._logger = logger;
}

public void OnException(ExceptionContext context)
{
// get default response message
var message = GetDefaultRequestErrorMessage(context.HttpContext.Request);

if (context.Exception is BaseException)
{
var ex = context.Exception as BaseException;
var errorResult = new ValidationErrorResult(message, ex.Errors);
this.BuildResponse(context, ex.GetObjectResult(errorResult));
}
else
{
// ... error 500 logic here
}
}
}

It’s a simple but evolvable structure that resolves duplication in code, cleaning the regular API solution.

More info: Github Source