[c#] ASP.NET Core Web API exception handling

I am using ASP.NET Core for my new REST API project after using regular ASP.NET Web API for many years. I don't see any good way to handle exceptions in ASP.NET Core Web API. I tried to implement exception handling filter/attribute:

public class ErrorHandlingFilter : ExceptionFilterAttribute
{
    public override void OnException(ExceptionContext context)
    {
        HandleExceptionAsync(context);
        context.ExceptionHandled = true;
    }

    private static void HandleExceptionAsync(ExceptionContext context)
    {
        var exception = context.Exception;

        if (exception is MyNotFoundException)
            SetExceptionResult(context, exception, HttpStatusCode.NotFound);
        else if (exception is MyUnauthorizedException)
            SetExceptionResult(context, exception, HttpStatusCode.Unauthorized);
        else if (exception is MyException)
            SetExceptionResult(context, exception, HttpStatusCode.BadRequest);
        else
            SetExceptionResult(context, exception, HttpStatusCode.InternalServerError);
    }

    private static void SetExceptionResult(
        ExceptionContext context, 
        Exception exception, 
        HttpStatusCode code)
    {
        context.Result = new JsonResult(new ApiResponse(exception))
        {
            StatusCode = (int)code
        };
    }
}

And here is my Startup filter registration:

services.AddMvc(options =>
{
    options.Filters.Add(new AuthorizationFilter());
    options.Filters.Add(new ErrorHandlingFilter());
});

The issue I was having is that when exception occurres in my AuthorizationFilter it's not being handled by ErrorHandlingFilter. I was expecting it to be caught there just like it worked with old ASP.NET Web API.

So how can I catch all application exceptions as well as any exceptions from Action Filters?

This question is related to c# exception asp.net-core

The answer is


If you want set custom exception handling behavior for a specific controller, you can do so by overriding the controllers OnActionExecuted method.

Remember to set the ExceptionHandled property to true to disable default exception handling behavior.

Here is a sample from an api I'm writing, where I want to catch specific types of exceptions and return a json formatted result:

    private static readonly Type[] API_CATCH_EXCEPTIONS = new Type[]
    {
        typeof(InvalidOperationException),
        typeof(ValidationException)           
    };

    public override void OnActionExecuted(ActionExecutedContext context)
    {
        base.OnActionExecuted(context);

        if (context.Exception != null)
        {
            var exType = context.Exception.GetType();
            if (API_CATCH_EXCEPTIONS.Any(type => exType == type || exType.IsSubclassOf(type)))
            {
                context.Result = Problem(detail: context.Exception.Message);
                context.ExceptionHandled = true;
            }
        }  
    }

A simple way to handle an exception on any particular method is:

using Microsoft.AspNetCore.Http;
...

public ActionResult MyAPIMethod()
{
    try
    {
       var myObject = ... something;

       return Json(myObject);
    }
    catch (Exception ex)
    {
        Log.Error($"Error: {ex.Message}");
        return StatusCode(StatusCodes.Status500InternalServerError);
    }         
}

First, configure ASP.NET Core 2 Startup to re-execute to an error page for any errors from the web server and any unhandled exceptions.

public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
    if (env.IsDevelopment()) {
        // Debug config here...
    } else {
        app.UseStatusCodePagesWithReExecute("/Error");
        app.UseExceptionHandler("/Error");
    }
    // More config...
}

Next, define an exception type that will let you throw errors with HTTP status codes.

public class HttpException : Exception
{
    public HttpException(HttpStatusCode statusCode) { StatusCode = statusCode; }
    public HttpStatusCode StatusCode { get; private set; }
}

Finally, in your controller for the error page, customize the response based on the reason for the error and whether the response will be seen directly by an end user. This code assumes all API URLs start with /api/.

[AllowAnonymous]
public IActionResult Error()
{
    // Gets the status code from the exception or web server.
    var statusCode = HttpContext.Features.Get<IExceptionHandlerFeature>()?.Error is HttpException httpEx ?
        httpEx.StatusCode : (HttpStatusCode)Response.StatusCode;

    // For API errors, responds with just the status code (no page).
    if (HttpContext.Features.Get<IHttpRequestFeature>().RawTarget.StartsWith("/api/", StringComparison.Ordinal))
        return StatusCode((int)statusCode);

    // Creates a view model for a user-friendly error page.
    string text = null;
    switch (statusCode) {
        case HttpStatusCode.NotFound: text = "Page not found."; break;
        // Add more as desired.
    }
    return View("Error", new ErrorViewModel { RequestId = Activity.Current?.Id ?? HttpContext.TraceIdentifier, ErrorText = text });
}

ASP.NET Core will log the error detail for you to debug with, so a status code may be all you want to provide to a (potentially untrusted) requester. If you want to show more info, you can enhance HttpException to provide it. For API errors, you can put JSON-encoded error info in the message body by replacing return StatusCode... with return Json....


Your best bet is to use middleware to achieve logging you're looking for. You want to put your exception logging in one middleware and then handle your error pages displayed to the user in a different middleware. That allows separation of logic and follows the design Microsoft has laid out with the 2 middleware components. Here's a good link to Microsoft's documentation: Error Handling in ASP.Net Core

For your specific example, you may want to use one of the extensions in the StatusCodePage middleware or roll your own like this.

You can find an example here for logging exceptions: ExceptionHandlerMiddleware.cs

public void Configure(IApplicationBuilder app)
{
    // app.UseErrorPage(ErrorPageOptions.ShowAll);
    // app.UseStatusCodePages();
    // app.UseStatusCodePages(context => context.HttpContext.Response.SendAsync("Handler, status code: " + context.HttpContext.Response.StatusCode, "text/plain"));
    // app.UseStatusCodePages("text/plain", "Response, status code: {0}");
    // app.UseStatusCodePagesWithRedirects("~/errors/{0}");
    // app.UseStatusCodePagesWithRedirects("/base/errors/{0}");
    // app.UseStatusCodePages(builder => builder.UseWelcomePage());
    app.UseStatusCodePagesWithReExecute("/Errors/{0}");  // I use this version

    // Exception handling logging below
    app.UseExceptionHandler();
}

If you don't like that specific implementation, then you can also use ELM Middleware, and here are some examples: Elm Exception Middleware

public void Configure(IApplicationBuilder app)
{
    app.UseStatusCodePagesWithReExecute("/Errors/{0}");
    // Exception handling logging below
    app.UseElmCapture();
    app.UseElmPage();
}

If that doesn't work for your needs, you can always roll your own Middleware component by looking at their implementations of the ExceptionHandlerMiddleware and the ElmMiddleware to grasp the concepts for building your own.

It's important to add the exception handling middleware below the StatusCodePages middleware but above all your other middleware components. That way your Exception middleware will capture the exception, log it, then allow the request to proceed to the StatusCodePage middleware which will display the friendly error page to the user.


Firstly, thanks to Andrei as I've based my solution on his example.

I'm including mine as it's a more complete sample and might save readers some time.

The limitation of Andrei's approach is that doesn't handle logging, capturing potentially useful request variables and content negotiation (it will always return JSON no matter what the client has requested - XML / plain text etc).

My approach is to use an ObjectResult which allows us to use the functionality baked into MVC.

This code also prevents caching of the response.

The error response has been decorated in such a way that it can be serialized by the XML serializer.

public class ExceptionHandlerMiddleware
{
    private readonly RequestDelegate next;
    private readonly IActionResultExecutor<ObjectResult> executor;
    private readonly ILogger logger;
    private static readonly ActionDescriptor EmptyActionDescriptor = new ActionDescriptor();

    public ExceptionHandlerMiddleware(RequestDelegate next, IActionResultExecutor<ObjectResult> executor, ILoggerFactory loggerFactory)
    {
        this.next = next;
        this.executor = executor;
        logger = loggerFactory.CreateLogger<ExceptionHandlerMiddleware>();
    }

    public async Task Invoke(HttpContext context)
    {
        try
        {
            await next(context);
        }
        catch (Exception ex)
        {
            logger.LogError(ex, $"An unhandled exception has occurred while executing the request. Url: {context.Request.GetDisplayUrl()}. Request Data: " + GetRequestData(context));

            if (context.Response.HasStarted)
            {
                throw;
            }

            var routeData = context.GetRouteData() ?? new RouteData();

            ClearCacheHeaders(context.Response);

            var actionContext = new ActionContext(context, routeData, EmptyActionDescriptor);

            var result = new ObjectResult(new ErrorResponse("Error processing request. Server error."))
            {
                StatusCode = (int) HttpStatusCode.InternalServerError,
            };

            await executor.ExecuteAsync(actionContext, result);
        }
    }

    private static string GetRequestData(HttpContext context)
    {
        var sb = new StringBuilder();

        if (context.Request.HasFormContentType && context.Request.Form.Any())
        {
            sb.Append("Form variables:");
            foreach (var x in context.Request.Form)
            {
                sb.AppendFormat("Key={0}, Value={1}<br/>", x.Key, x.Value);
            }
        }

        sb.AppendLine("Method: " + context.Request.Method);

        return sb.ToString();
    }

    private static void ClearCacheHeaders(HttpResponse response)
    {
        response.Headers[HeaderNames.CacheControl] = "no-cache";
        response.Headers[HeaderNames.Pragma] = "no-cache";
        response.Headers[HeaderNames.Expires] = "-1";
        response.Headers.Remove(HeaderNames.ETag);
    }

    [DataContract(Name= "ErrorResponse")]
    public class ErrorResponse
    {
        [DataMember(Name = "Message")]
        public string Message { get; set; }

        public ErrorResponse(string message)
        {
            Message = message;
        }
    }
}

use middleware or IExceptionHandlerPathFeature is fine. there is another way in eshop

create a exceptionfilter and register it

public class HttpGlobalExceptionFilter : IExceptionFilter
{
  public void OnException(ExceptionContext context)
  {...}
}
services.AddMvc(options =>
{
  options.Filters.Add(typeof(HttpGlobalExceptionFilter));
})

To Configure exception handling behavior per exception type you can use Middleware from NuGet packages:

Code sample:

public void ConfigureServices(IServiceCollection services)
{
    services.AddMvc();

    services.AddExceptionHandlingPolicies(options =>
    {
        options.For<InitializationException>().Rethrow();

        options.For<SomeTransientException>().Retry(ro => ro.MaxRetryCount = 2).NextPolicy();

        options.For<SomeBadRequestException>()
        .Response(e => 400)
            .Headers((h, e) => h["X-MyCustomHeader"] = e.Message)
            .WithBody((req,sw, exception) =>
                {
                    byte[] array = Encoding.UTF8.GetBytes(exception.ToString());
                    return sw.WriteAsync(array, 0, array.Length);
                })
        .NextPolicy();

        // Ensure that all exception types are handled by adding handler for generic exception at the end.
        options.For<Exception>()
        .Log(lo =>
            {
                lo.EventIdFactory = (c, e) => new EventId(123, "UnhandlerException");
                lo.Category = (context, exception) => "MyCategory";
            })
        .Response(null, ResponseAlreadyStartedBehaviour.GoToNextHandler)
            .ClearCacheHeaders()
            .WithObjectResult((r, e) => new { msg = e.Message, path = r.Path })
        .Handled();
    });
}

public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
    app.UseExceptionHandlingPolicies();
    app.UseMvc();
}

The well-accepted answer helped me a lot but I wanted to pass HttpStatusCode in my middleware to manage error status code at runtime.

According to this link I got some idea to do the same. So I merged the Andrei Answer with this. So my final code is below:


1. Base class

public class ErrorDetails
{
    public int StatusCode { get; set; }
    public string Message { get; set; }

    public override string ToString()
    {
        return JsonConvert.SerializeObject(this);
    }
}


2. Custom Exception Class Type

public class HttpStatusCodeException : Exception
{
    public HttpStatusCode StatusCode { get; set; }
    public string ContentType { get; set; } = @"text/plain";

    public HttpStatusCodeException(HttpStatusCode statusCode)
    {
        this.StatusCode = statusCode;
    }

    public HttpStatusCodeException(HttpStatusCode statusCode, string message) 
        : base(message)
    {
        this.StatusCode = statusCode;
    }

    public HttpStatusCodeException(HttpStatusCode statusCode, Exception inner) 
        : this(statusCode, inner.ToString()) { }

    public HttpStatusCodeException(HttpStatusCode statusCode, JObject errorObject) 
        : this(statusCode, errorObject.ToString())
    {
        this.ContentType = @"application/json";
    }

}


3. Custom Exception Middleware

public class CustomExceptionMiddleware
{
    private readonly RequestDelegate next;

    public CustomExceptionMiddleware(RequestDelegate next)
    {
        this.next = next;
    }

    public async Task Invoke(HttpContext context /* other dependencies */)
    {
        try
        {
            await next(context);
        }
        catch (HttpStatusCodeException ex)
        {
            await HandleExceptionAsync(context, ex);
        }
        catch (Exception exceptionObj)
        {
            await HandleExceptionAsync(context, exceptionObj);
        }
    }

    private Task HandleExceptionAsync(HttpContext context, HttpStatusCodeException exception)
    {
        string result = null;
        context.Response.ContentType = "application/json";
        if (exception is HttpStatusCodeException)
        {
            result = new ErrorDetails() 
            {
                Message = exception.Message,
                StatusCode = (int)exception.StatusCode 
            }.ToString();
            context.Response.StatusCode = (int)exception.StatusCode;
        }
        else
        {
            result = new ErrorDetails() 
            { 
                Message = "Runtime Error",
                StatusCode = (int)HttpStatusCode.BadRequest
            }.ToString();
            context.Response.StatusCode = (int)HttpStatusCode.BadRequest;
        }
        return context.Response.WriteAsync(result);
    }

    private Task HandleExceptionAsync(HttpContext context, Exception exception)
    {
        string result = new ErrorDetails() 
        { 
            Message = exception.Message,
            StatusCode = (int)HttpStatusCode.InternalServerError 
        }.ToString();
        context.Response.StatusCode = (int)HttpStatusCode.BadRequest;
        return context.Response.WriteAsync(result);
    }
}


4. Extension Method

public static void ConfigureCustomExceptionMiddleware(this IApplicationBuilder app)
{
    app.UseMiddleware<CustomExceptionMiddleware>();
}

5. Configure Method in startup.cs

app.ConfigureCustomExceptionMiddleware();
app.UseMvc();

Now my login method in Account controller :

try
{
    IRepository<UserMaster> obj 
        = new Repository<UserMaster>(_objHeaderCapture, Constants.Tables.UserMaster);
    var result = obj.Get()
        .AsQueryable()
        .Where(sb => sb.EmailId.ToLower() == objData.UserName.ToLower() 
            && sb.Password == objData.Password.ToEncrypt() 
            && sb.Status == (int)StatusType.Active)
        .FirstOrDefault();
    if (result != null)//User Found
        return result;
    else // Not Found
        throw new HttpStatusCodeException(HttpStatusCode.NotFound,
            "Please check username or password");
}
catch (Exception ex)
{
    throw ex;
}

Above you can see if i have not found the user then raising the HttpStatusCodeException in which i have passed HttpStatusCode.NotFound status and a custom message
In middleware

catch (HttpStatusCodeException ex)

blocked will be called which will pass control to

private Task HandleExceptionAsync(HttpContext context, HttpStatusCodeException exception) method


But what if i got runtime error before? For that i have used try catch block which throw exception and will be catched in catch (Exception exceptionObj) block and will pass control to

Task HandleExceptionAsync(HttpContext context, Exception exception)

method.

I have used a single ErrorDetails class for uniformity.


There is a built-in middleware that makes it easier than writing a custom one.

Asp.Net Core 5 version:

app.UseExceptionHandler(a => a.Run(async context =>
{
    var exceptionHandlerPathFeature = context.Features.Get<IExceptionHandlerPathFeature>();
    var exception = exceptionHandlerPathFeature.Error;
    
    await context.Response.WriteAsJsonAsync(new { error = exception.Message });
}));

Older versions (they did not have WriteAsJsonAsync extension):

app.UseExceptionHandler(a => a.Run(async context =>
{
    var exceptionHandlerPathFeature = context.Features.Get<IExceptionHandlerPathFeature>();
    var exception = exceptionHandlerPathFeature.Error;
    
    var result = JsonConvert.SerializeObject(new { error = exception.Message });
    context.Response.ContentType = "application/json";
    await context.Response.WriteAsync(result);
}));

It should do pretty much the same, just a bit less code to write.

Important: Remember to add it before UseMvc (or UseRouting in .Net Core 3) as order is important.


By adding your own "Exception Handling Middleware", makes it hard to reuse some good built-in logic of Exception Handler like send an "RFC 7807-compliant payload to the client" when an error happens.

What I made was to extend built-in Exception handler outside of the Startup.cs class to handle custom exceptions or override the behavior of existing ones. For example, an ArgumentException and convert into BadRequest without changing the default behavior of other exceptions:

on the Startup.cs add:

app.UseExceptionHandler("/error");

and extend ErrorController.cs with something like this:

using System;
using Microsoft.AspNetCore.Diagnostics;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Hosting;

namespace Api.Controllers
{
    [ApiController]
    [ApiExplorerSettings(IgnoreApi = true)]
    [AllowAnonymous]
    public class ErrorController : ControllerBase
    {
        [Route("/error")]
        public IActionResult Error(
            [FromServices] IWebHostEnvironment webHostEnvironment)
        {
            var context = HttpContext.Features.Get<IExceptionHandlerFeature>();
            var exceptionType = context.Error.GetType();
            
            if (exceptionType == typeof(ArgumentException)
                || exceptionType == typeof(ArgumentNullException)
                || exceptionType == typeof(ArgumentOutOfRangeException))
            {
                if (webHostEnvironment.IsDevelopment())
                {
                    return ValidationProblem(
                        context.Error.StackTrace,
                        title: context.Error.Message);
                }

                return ValidationProblem(context.Error.Message);
            }

            if (exceptionType == typeof(NotFoundException))
            {
                return NotFound(context.Error.Message);
            }

            if (webHostEnvironment.IsDevelopment())
            {
                return Problem(
                    context.Error.StackTrace,
                    title: context.Error.Message
                    );
            }
            
            return Problem();
        }
    }
}

Note that:

  1. NotFoundException is a custom exception and all you need to do is throw new NotFoundException(null); or throw new ArgumentException("Invalid argument.");
  2. You should not serve sensitive error information to clients. Serving errors is a security risk.

Examples related to c#

How can I convert this one line of ActionScript to C#? Microsoft Advertising SDK doesn't deliverer ads How to use a global array in C#? How to correctly write async method? C# - insert values from file into two arrays Uploading into folder in FTP? Are these methods thread safe? dotnet ef not found in .NET Core 3 HTTP Error 500.30 - ANCM In-Process Start Failure Best way to "push" into C# array

Examples related to exception

Connection Java-MySql : Public Key Retrieval is not allowed How to print an exception in Python 3? ASP.NET Core Web API exception handling Catching FULL exception message How to get exception message in Python properly What does "Fatal error: Unexpectedly found nil while unwrapping an Optional value" mean? what does Error "Thread 1:EXC_BAD_INSTRUCTION (code=EXC_I386_INVOP, subcode=0x0)" mean? Argument Exception "Item with Same Key has already been added" The given key was not present in the dictionary. Which key? sql try/catch rollback/commit - preventing erroneous commit after rollback

Examples related to asp.net-core

dotnet ef not found in .NET Core 3 How to use Bootstrap 4 in ASP.NET Core ASP.NET Core - Swashbuckle not creating swagger.json file Getting value from appsettings.json in .net core .net Core 2.0 - Package was restored using .NetFramework 4.6.1 instead of target framework .netCore 2.0. The package may not be fully compatible Automatically set appsettings.json for dev and release environments in asp.net core? Get ConnectionString from appsettings.json instead of being hardcoded in .NET Core 2.0 App Unable to create migrations after upgrading to ASP.NET Core 2.0 EF Core add-migration Build Failed ASP.NET Core form POST results in a HTTP 415 Unsupported Media Type response