A web API that returns JSON responses should be expected to return errors or exceptions as JSON messages, too. You can use exception filters in ASP.NET Core MVC to trap and serialize exceptions that occur within MVC. However, if an exception is thrown before (or after) the MVC pipeline, it won’t be handled by the filter and the client will get an ugly error message (or a 500 Internal Server Error).

A more universal solution that can trap and serialize any exception that happens during a request is an exception handling middleware component. The syntax is a little different than an exception filter, but the principle is the same.

In this post, I’ll show you how to write error handling middleware and how to extend it with custom behavior.

Exception middleware

The basic pattern for any ASP.NET Core middleware is a class with an Invoke method:

public class BasicMiddleware
{
    public Task Invoke(HttpContext context)
    {
        // do something with context
    }
}

You can’t use catch to trap an exception here, because the exception has already occured by the time the middleware is called. Instead, use IExceptionHandlerFeature.Error to retrieve it.

Here’s a working example that uses JSON.NET to serialize a custom response:

public class JsonExceptionMiddleware
{
    public async Task Invoke(HttpContext context)
    {
        context.Response.StatusCode = (int)HttpStatusCode.InternalServerError;

        var ex = context.Features.Get<IExceptionHandlerFeature>()?.Error;
        if (ex == null) return;

        var error = new 
        {
            message = ex.Message
        };

        context.Response.ContentType = "application/json";

        using (var writer = new StreamWriter(context.Response.Body))
        {
            new JsonSerializer().Serialize(writer, error);
            await writer.FlushAsync().ConfigureAwait(false);
        }
    }
}

The UseExceptionHandler method in the Configure method of the Startup class adds your exception handling middleware to the ASP.NET Core pipeline. Since order matters for middleware, make sure you wire it up above the UseMvc line:

public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
    // other code...
    
    app.UseExceptionHandler(new ExceptionHandlerOptions 
    {
        ExceptionHandler = new JsonExceptionMiddleware().Invoke
    });
    
    app.UseMvc();
}

Now your middleware will be invoked whenever an unhandled exception occurs anywhere in your ASP.NET Core application.

Return more detail in development

It’s a good idea to return generic error messages in production, so you don’t leak information about your application. However, in development, the more detail the better.

You can use IHostingEnvironment in the Configure method to determine whether ASP.NET Core is running in development or production mode. It can be passed to your middleware component:

// env is injected in the Configure method constructor
app.UseExceptionHandler(new ExceptionHandlerOptions 
{
    ExceptionHandler = new JsonExceptionMiddleware(env).Invoke
});

By passing IHostingEnvironment to your exception handling middleware, the middleware code can choose whether to respond with a generic message, or the full exception message and stack trace:

if (env.IsDevelopment())
{
    error.Message = ex.Message;
    error.Detail = ex.StackTrace;
}
else
{
    error.Message = DefaultErrorMessage;
    error.Detail = ex.Message;
}

Alternatively, you could perform this check in the Configure method, and add different middleware to the pipeline if the app was in development versus production mode.

There’s a full example of this exception handler class in my Beautiful REST API example repo. Check out JsonExceptionMiddleware.cs if you’re curious!