Asp net global error handling

Find out how to replace try-catch blocks in your code with the Global Error Handling by using the custom or built-in middleware in ASP.NET Core.

The exception handling features help us deal with the unforeseen errors which could appear in our code.  To handle exceptions we can use the try-catch block in our code as well as finally keyword to clean up resources afterward.

Even though there is nothing wrong with the try-catch blocks in our Actions in Web API project, we can extract all the exception handling logic into a single centralized place. By doing that, we make our actions more readable and the error handling process more maintainable. If we want to make our actions even more readable and maintainable, we can implement Action Filters. We won’t talk about action filters in this article but we strongly recommend reading our post Action Filters in .NET Core.

In this article, we are going to handle errors by using a try-catch block first and then rewrite our code by using built-in middleware and our custom middleware for global error handling to demonstrate the benefits of this approach. We are going to use an ASP.NET Core Web API project to explain these features and if you want to learn more about it (which we strongly recommend), you can read our ASP.NET Core Web API Tutorial.


VIDEO: Global Error Handling in ASP.NET Core Web API video.


To download the source code for our starting project, you can visit the Global error handling start project.

For the finished project refer to Global error handling end project.

Let’s start.

Error Handling With Try-Catch Block

To start off with this example, let’s open the Values Controller from the starting project (Global-Error-Handling-Start project). In this project, we can find a single Get() method and an injected Logger service.

It is a common practice to include the log messages while handling errors, therefore we have created the LoggerManager service. It logs all the messages to the C drive, but you can change that by modifying the path in the nlog.config file. For more information about how to use Nlog in .NET Core, you can visit Logging with NLog.

Now, let’s modify our action method to return a result and log some messages:

using System;
using LoggerService;
using Microsoft.AspNetCore.Mvc;

namespace GlobalErrorHandling.Controllers
{
    [Route("api/[controller]")]
    [ApiController]
    public class ValuesController : ControllerBase
    {
        private ILoggerManager _logger;

        public ValuesController(ILoggerManager logger)
        {
            _logger = logger;
        }

        [HttpGet]
        public IActionResult Get()
        {
            try
            {
                _logger.LogInfo("Fetching all the Students from the storage");

                var students = DataManager.GetAllStudents(); //simulation for the data base access

                _logger.LogInfo($"Returning {students.Count} students.");

                return Ok(students);
            }
            catch (Exception ex)
            {
                _logger.LogError($"Something went wrong: {ex}");
                return StatusCode(500, "Internal server error");
            }
        }
    }
}

When we send a request at this endpoint, we will get this result:

Basic request - Global Error Handling

And the log messages:

log basic request - Global Error Handling

Wanna join Code Maze Team, help us produce more awesome .NET/C# content and get paid? >> JOIN US! <<

We see that everything is working as expected.

Now let’s modify our code, right below the GetAllStudents() method call, to force an exception:

throw new Exception("Exception while fetching all the students from the storage.");

Now, if we send a request:

try catche error - Global Error Handling

And the log messages:

log try catch error

So, this works just fine. But the downside of this approach is that we need to repeat our try-catch blocks in all the actions in which we want to catch unhandled exceptions. Well, there is a better approach to do that.

Handling Errors Globally With the Built-In Middleware

The UseExceptionHandler middleware is a built-in middleware that we can use to handle exceptions in our ASP.NET Core Web API application. So, let’s dive into the code to see this middleware in action.

Wanna join Code Maze Team, help us produce more awesome .NET/C# content and get paid? >> JOIN US! <<

First, we are going to add a new class ErrorDetails in the Models folder:

using System.Text.Json;

namespace GlobalErrorHandling.Models
{
    public class ErrorDetails
    {
        public int StatusCode { get; set; }
        public string Message { get; set; }

        public override string ToString()
        {
            return JsonSerializer.Serialize(this);
        }
    }
}

We are going to use this class for the details of our error message.

To continue, let’s create a new folder Extensions and a new static class ExceptionMiddlewareExtensions.cs inside it.

Now, we need to modify it:

using GlobalErrorHandling.Models;
using LoggerService;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Diagnostics;
using Microsoft.AspNetCore.Http;
using System.Net;

namespace GlobalErrorHandling.Extensions
{
    public static class ExceptionMiddlewareExtensions
    {
        public static void ConfigureExceptionHandler(this IApplicationBuilder app, ILoggerManager logger)
        {
            app.UseExceptionHandler(appError =>
            {
                appError.Run(async context =>
                {
                    context.Response.StatusCode = (int)HttpStatusCode.InternalServerError;
                    context.Response.ContentType = "application/json";

                    var contextFeature = context.Features.Get<IExceptionHandlerFeature>();
                    if(contextFeature != null)
                    { 
                        logger.LogError($"Something went wrong: {contextFeature.Error}");

                        await context.Response.WriteAsync(new ErrorDetails()
                        {
                            StatusCode = context.Response.StatusCode,
                            Message = "Internal Server Error."
                        }.ToString());
                    }
                });
            });
        }
    }
}

In the code above, we’ve created an extension method in which we’ve registered the UseExceptionHandler middleware. Then, we populate the status code and the content type of our response, log the error message and finally return the response with the custom-created object.

To be able to use this extension method, let’s modify the Configure method inside the Startup class for .NET 5 project:

public void Configure(IApplicationBuilder app, IWebHostEnvironment env, ILoggerManager logger) 
{
    if (env.IsDevelopment())
    {
        app.UseDeveloperExceptionPage();
    }

    app.ConfigureExceptionHandler(logger);

    app.UseHttpsRedirection();

    app.UseRouting();

    app.UseAuthorization();

    app.UseEndpoints(endpoints =>
    {
        endpoints.MapControllers();
    });
}

Or if you are using .NET 6 and above:

Wanna join Code Maze Team, help us produce more awesome .NET/C# content and get paid? >> JOIN US! <<

var app = builder.Build();

var logger = app.Services.GetRequiredService<ILoggerManager>();
app.ConfigureExceptionHandler(logger);

Finally, let’s remove the try-catch block from our code:

public IActionResult Get()
{
    _logger.LogInfo("Fetching all the Students from the storage");

     var students = DataManager.GetAllStudents(); //simulation for the data base access

     throw new Exception("Exception while fetching all the students from the storage.");

     _logger.LogInfo($"Returning {students.Count} students.");

     return Ok(students);
}

And there you go. Our action method is much cleaner now and what’s more important we can reuse this functionality to write more readable actions in the future.

So let’s inspect the result:

Global Handler Middleware

And the log messages:

log global handler middleware

Excellent.

Now, we are going to use custom middleware for global error handling.

Handling Errors Globally With the Custom Middleware

Let’s create a new folder named CustomExceptionMiddleware and a class ExceptionMiddleware.cs inside it.

We are going to modify that class:

public class ExceptionMiddleware
{
    private readonly RequestDelegate _next;
    private readonly ILoggerManager _logger;

    public ExceptionMiddleware(RequestDelegate next, ILoggerManager logger)
    {
        _logger = logger;
        _next = next;
    }

    public async Task InvokeAsync(HttpContext httpContext)
    {
        try
        {
            await _next(httpContext);
        }
        catch (Exception ex)
        {
            _logger.LogError($"Something went wrong: {ex}");
            await HandleExceptionAsync(httpContext, ex);
        }
    }

    private async Task HandleExceptionAsync(HttpContext context, Exception exception)
    {
        context.Response.ContentType = "application/json";
        context.Response.StatusCode = (int)HttpStatusCode.InternalServerError;

        await context.Response.WriteAsync(new ErrorDetails()
        {
            StatusCode = context.Response.StatusCode,
            Message = "Internal Server Error from the custom middleware."
        }.ToString());
    }
}

The first thing we need to do is to register our IloggerManager service and RequestDelegate through the dependency injection. The _next parameter of RequestDeleagate type is a function delegate that can process our HTTP requests.

After the registration process, we create the InvokeAsync() method. RequestDelegate can’t process requests without it.

If everything goes well, the _next delegate should process the request and the Get action from our controller should generate a successful response. But if a request is unsuccessful (and it is, because we are forcing an exception), our middleware will trigger the catch block and call the HandleExceptionAsync method.

In that method, we just set up the response status code and content type and return a response.

Now let’s modify our ExceptionMiddlewareExtensions class with another static method:

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

In .NET 6 and above, we have to extend the WebApplication type:

Wanna join Code Maze Team, help us produce more awesome .NET/C# content and get paid? >> JOIN US! <<

public static void ConfigureCustomExceptionMiddleware(this WebApplication app) 
{ 
    app.UseMiddleware<ExceptionMiddleware>(); 
}

Finally, let’s use this method in the Configure method in the Startup class:

//app.ConfigureExceptionHandler(logger);
app.ConfigureCustomExceptionMiddleware();

Great.

Now let’s inspect the result again:

custom handler middleware

There we go. Our custom middleware is implemented in a couple of steps.

Customizing Error Messages

If you want, you can always customize your error messages from the error handler. There are different ways of doing that, but we are going to show you the basic two ways.

First of all, we can assume that the AccessViolationException is thrown from our action:

[HttpGet]
public IActionResult Get()
{
    _logger.LogInfo("Fetching all the Students from the storage");

    var students = DataManager.GetAllStudents(); //simulation for the data base access

    throw new AccessViolationException("Violation Exception while accessing the resource.");

    _logger.LogInfo($"Returning {students.Count} students.");

    return Ok(students);
}

Now, what we can do is modify the InvokeAsync method inside the ExceptionMiddleware.cs class by adding a specific exception checking in the additional catch block:

Wanna join Code Maze Team, help us produce more awesome .NET/C# content and get paid? >> JOIN US! <<

public async Task InvokeAsync(HttpContext httpContext)
{
    try
    {
        await _next(httpContext);
    }
    catch (AccessViolationException avEx)
    {
        _logger.LogError($"A new violation exception has been thrown: {avEx}");
        await HandleExceptionAsync(httpContext, avEx);
    }
    catch (Exception ex)
    {
        _logger.LogError($"Something went wrong: {ex}");
        await HandleExceptionAsync(httpContext, ex);
    }
}

And that’s all. Now if we send another request with Postman, we are going to see in the log file that the AccessViolationException message is logged. Of course, our specific exception check must be placed before the global catch block.

With this solution, we are logging specific messages for the specific exceptions, and that can help us, as developers, a lot when we publish our application. But if we want to send a different message for a specific error, we can also modify the HandleExceptionAsync method in the same class:

private async Task HandleExceptionAsync(HttpContext context, Exception exception)
{
    context.Response.ContentType = "application/json";
    context.Response.StatusCode = (int)HttpStatusCode.InternalServerError;

    var message = exception switch
    {
        AccessViolationException =>  "Access violation error from the custom middleware",
        _ => "Internal Server Error from the custom middleware."
    };

    await context.Response.WriteAsync(new ErrorDetails()
    {
        StatusCode = context.Response.StatusCode,
        Message = message
    }.ToString());
}

Here, we are using a switch expression pattern matching to check the type of our exception and assign the right message to the message variable. Then, we just use that variable in the WriteAsync method.

Now if we test this, we will get a log message with the Access violation message, and our response will have a new message as well:

{
    "StatusCode": 500,
    "Message": "Access violation error from the custom middleware"
}

One thing to mention here. We are using the 500 status code for all the responses from the exception middleware, and that is something we believe it should be done. After all, we are handling exceptions and these exceptions should be marked with a 500 status code. But this doesn’t have to be the case all the time. For example, if you have a service layer and you want to propagate responses from the service methods as custom exceptions and catch them inside the global exception handler, you may want to choose a more appropriate status code for the response. You can read more about this technique in our Onion Architecture article. It really depends on your project organization.

Conclusion

That was awesome.

We have learned, how to handle errors in a more sophisticated way and cleaner as well. The code is much more readable and our exception handling logic is now reusable for the entire project.

Thank you for reading this article. We hope you have learned new useful things.

Wanna join Code Maze Team, help us produce more awesome .NET/C# content and get paid? >> JOIN US! <<

Время прочтения
6 мин

Просмотры 9.1K

В преддверии старта курса «C# ASP.NET Core разработчик» подготовили традиционный перевод полезного материала.

Также рекомендуем посмотреть вебинар на тему

«Отличия структурных шаблонов проектирования на примерах». На этом открытом уроке участники вместе с преподавателем-экспертом познакомятся с тремя структурными шаблонами проектирования: Заместитель, Адаптер и Декоратор.


Введение 

Сегодня в этой статье мы обсудим концепцию обработки исключений в приложениях ASP.NET Core. Обработка исключений (exception handling) — одна из наиболее важных импортируемых функций или частей любого типа приложений, которой всегда следует уделять внимание и правильно реализовывать. Исключения — это в основном средства ориентированные на обработку рантайм ошибок, которые возникают во время выполнения приложения. Если этот тип ошибок не обрабатывать должным образом, то приложение будет остановлено в результате их появления.

В ASP.NET Core концепция обработки исключений подверглась некоторым изменениям, и теперь она, если можно так сказать, находится в гораздо лучшей форме для внедрения обработки исключений. Для любых API-проектов реализация обработки исключений для каждого действия будет отнимать довольно много времени и дополнительных усилий. Но мы можем реализовать глобальный обработчик исключений (Global Exception handler), который будет перехватывать все типы необработанных исключений. Преимущество реализации глобального обработчика исключений состоит в том, что нам нужно определить его всего лишь в одном месте. Через этот обработчик будет обрабатываться любое исключение, возникающее в нашем приложении, даже если мы объявляем новые методы или контроллеры. Итак, в этой статье мы обсудим, как реализовать глобальную обработку исключений в ASP.NET Core Web API.

Создание проекта ASP.NET Core Web API в Visual Studio 2019

Итак, прежде чем переходить к обсуждению глобального обработчика исключений, сначала нам нужно создать проект ASP.NET Web API. Для этого выполните шаги, указанные ниже.

  • Откройте Microsoft Visual Studio и нажмите «Create a New Project» (Создать новый проект).

  • В диалоговом окне «Create New Project» выберите «ASP.NET Core Web Application for C#» (Веб-приложение ASP.NET Core на C#) и нажмите кнопку «Next» (Далее).

  • В окне «Configure your new project» (Настроить новый проект) укажите имя проекта и нажмите кнопку «Create» (Создать).

  • В диалоговом окне «Create a New ASP.NET Core Web Application» (Создание нового веб-приложения ASP.NET Core) выберите «API» и нажмите кнопку «Create».

  • Убедитесь, что флажки «Enable Docker Support» (Включить поддержку Docker) и «Configure for HTTPS» (Настроить под HTTPS) сняты. Мы не будем использовать эти функции.

  • Убедитесь, что выбрано «No Authentication» (Без аутентификации), поскольку мы также не будем использовать аутентификацию.

  • Нажмите ОК.

Используем UseExceptionHandler middleware в ASP.NET Core.

Чтобы реализовать глобальный обработчик исключений, мы можем воспользоваться преимуществами встроенного Middleware ASP.NET Core. Middleware представляет из себя программный компонент, внедренный в конвейер обработки запросов, который каким-либо образом обрабатывает запросы и ответы. Мы можем использовать встроенное middleware ASP.NET Core UseExceptionHandler в качестве глобального обработчика исключений. Конвейер обработки запросов ASP.NET Core включает в себя цепочку middleware-компонентов. Эти компоненты, в свою очередь, содержат серию делегатов запросов, которые вызываются один за другим. В то время как входящие запросы проходят через каждый из middleware-компонентов в конвейере, каждый из этих компонентов может либо обработать запрос, либо передать запрос следующему компоненту в конвейере.

С помощью этого middleware мы можем получить всю детализированную информацию об объекте исключения, такую ​​как стектрейс, вложенное исключение, сообщение и т. д., а также вернуть эту информацию через API в качестве вывода. Нам нужно поместить middleware обработки исключений в configure() файла startup.cs. Если мы используем какое-либо приложение на основе MVC, мы можем использовать middleware обработки исключений, как это показано ниже. Этот фрагмент кода демонстрирует, как мы можем настроить middleware UseExceptionHandler для перенаправления пользователя на страницу с ошибкой при возникновении любого типа исключения.

public void Configure(IApplicationBuilder app, IWebHostEnvironment env)  
{  
    app.UseExceptionHandler("/Home/Error");  
    app.UseMvc();  
} 

Теперь нам нужно проверить сообщение об исключении. Для этого откройте файл WeatherForecastController.cs и добавьте следующий экшн-метод, чтобы пробросить исключение:

[Route("GetExceptionInfo")]  
[HttpGet]  
public IEnumerable<string> GetExceptionInfo()  
{  
     string[] arrRetValues = null;  
     if (arrRetValues.Length > 0)  
     { }  
     return arrRetValues;  
} 

Если мы хотим получить подробную информацию об объектах исключения, например, стектрейс, сообщение и т. д., мы можем использовать приведенный ниже код в качестве middleware исключения —

app.UseExceptionHandler(  
                options =>  
                {  
                    options.Run(  
                        async context =>  
                        {  
                            context.Response.StatusCode = (int)HttpStatusCode.InternalServerError;  
                            context.Response.ContentType = "text/html";  
                            var exceptionObject = context.Features.Get<IExceptionHandlerFeature>();  
                            if (null != exceptionObject)  
                            {  
                                var errorMessage = $"<b>Exception Error: {exceptionObject.Error.Message} </b> {exceptionObject.Error.StackTrace}";  
                                await context.Response.WriteAsync(errorMessage).ConfigureAwait(false);  
                            }  
                        });  
                }  
            );  

Для проверки вывода просто запустите эндпоинт API в любом браузере:

Определение пользовательского Middleware для обработки исключений в API ASP.NET Core

Кроме того, мы можем написать собственное middleware для обработки любых типов исключений. В этом разделе мы продемонстрируем, как создать типичный пользовательский класс middleware. Пользовательское middleware также обеспечивает гораздо большую гибкость для обработки исключений. Мы можем добавить стекатрейс, имя типа исключения, код ошибки или что-нибудь еще, что мы захотим включить как часть сообщения об ошибке. В приведенном ниже фрагменте кода показан типичный пользовательский класс middleware:

using Microsoft.AspNetCore.Http;    
using Newtonsoft.Json;    
using System;    
using System.Collections.Generic;    
using System.Linq;    
using System.Net;    
using System.Threading.Tasks;    
    
namespace API.DemoSample.Exceptions    
{    
    public class ExceptionHandlerMiddleware    
    {    
        private readonly RequestDelegate _next;    
    
        public ExceptionHandlerMiddleware(RequestDelegate next)    
        {    
            _next = next;    
        }    
    
        public async Task Invoke(HttpContext context)    
        {    
            try    
            {    
                await _next.Invoke(context);    
            }    
            catch (Exception ex)    
            {    
                    
            }    
        }    
    }    
} 

В приведенном выше классе делегат запроса передается любому middleware. Middleware либо обрабатывает его, либо передает его следующему middleware в цепочке. Если запрос не успешен, будет выброшено исключение, а затем будет выполнен метод HandleExceptionMessageAsync в блоке catch. Итак, давайте обновим код метода Invoke, как показано ниже:

public async Task Invoke(HttpContext context)  
{  
    try  
    {  
        await _next.Invoke(context);  
    }  
    catch (Exception ex)  
    {  
        await HandleExceptionMessageAsync(context, ex).ConfigureAwait(false);  
    }  
}  

 Теперь нам нужно реализовать метод HandleExceptionMessageAsync, как показано ниже:

private static Task HandleExceptionMessageAsync(HttpContext context, Exception exception)  
{  
    context.Response.ContentType = "application/json";  
    int statusCode = (int)HttpStatusCode.InternalServerError;  
    var result = JsonConvert.SerializeObject(new  
    {  
        StatusCode = statusCode,  
        ErrorMessage = exception.Message  
    });  
    context.Response.ContentType = "application/json";  
    context.Response.StatusCode = statusCode;  
    return context.Response.WriteAsync(result);  
} 

Теперь, на следующем шаге, нам нужно создать статический класс с именем ExceptionHandlerMiddlewareExtensions и добавить приведенный ниже код в этот класс,

using Microsoft.AspNetCore.Builder;  
using System;  
using System.Collections.Generic;  
using System.Linq;  
using System.Threading.Tasks;  
  
namespace API.DemoSample.Exceptions  
{  
    public static class ExceptionHandlerMiddlewareExtensions  
    {  
        public static void UseExceptionHandlerMiddleware(this IApplicationBuilder app)  
        {  
            app.UseMiddleware<ExceptionHandlerMiddleware>();  
        }  
    }  
}  

На последнем этапе, нам нужно включить наше пользовательское middleware в методе Configure класса startup, как показано ниже:

app.UseExceptionHandlerMiddleware();  

Заключение

Обработка исключений — это по сути сквозная функциональность для любого типа приложений. В этой статье мы обсудили процесс реализации концепции глобальной обработки исключений. Мы можем воспользоваться преимуществами глобальной обработки исключений в любом приложении ASP.NET Core, чтобы гарантировать, что каждое исключение будет перехвачено и вернет правильные сведения, связанные с этим исключением. С глобальной обработкой исключений нам достаточно в одном месте написать код, связанный с обработкой исключений, для всего нашего приложения. Любые предложения, отзывы или запросы, связанные с этой статьей, приветствуются.


Узнать подробнее о курсе «C# ASP.NET Core разработчик».

Посмотреть вебинар на тему «Отличия структурных шаблонов проектирования на примерах».

Built with ASP.NET Core 3.1

Other versions available:

  • .NET: .NET 6.0, 5.0
  • Next.js: Next.js 11

This is a quick post to show how to implement a global exception handler in ASP.NET Core 3.1.

These are the main pieces involved in the error handling process with a brief description of each:

  • Global Error Handler Middleware — custom middleware that catches and handles all exceptions, and determines which HTTP response code to return based on the exception type.
  • Custom App Exception — a custom exception class used to differentiate between handled exceptions that return a 400 response and unhandled exceptions that return a 500 response.
  • Startup.cs — the ASP.NET Core startup class that adds the global error handler middleware to the application request pipeline.
  • Example Error Service — an example service that shows how to throw a custom exception that will be handled by the global error handler.

The below example code snippets are from a boilerplate authentication api tutorial I posted recently, for the full tutorial or to download and test the code see ASP.NET Core 3.1 — Boilerplate API with Email Sign Up, Verification, Authentication & Forgot Password.

Global Error Handler Middleware

The global error handler middleware is used catch all exceptions thrown by the api in a single place, removing the need for duplicated error handling code throughout the application. It’s configured as middleware in the Configure method of the Startup.cs class.

Errors of type AppException are treated as custom (app specific) errors that return a 400 Bad Request response, the ASP.NET built-in KeyNotFoundException class is used to return 404 Not Found responses, all other exceptions are unhandled and return a 500 Internal Server Error response.

See the example service for examples of how to throw a custom app exception or not found exception that will be handled by the global error handler.

using Microsoft.AspNetCore.Http;
using System;
using System.Collections.Generic;
using System.Net;
using System.Text.Json;
using System.Threading.Tasks;
using WebApi.Helpers;

namespace WebApi.Middleware
{
    public class ErrorHandlerMiddleware
    {
        private readonly RequestDelegate _next;

        public ErrorHandlerMiddleware(RequestDelegate next)
        {
            _next = next;
        }

        public async Task Invoke(HttpContext context)
        {
            try
            {
                await _next(context);
            }
            catch (Exception error)
            {
                var response = context.Response;
                response.ContentType = "application/json";

                switch(error)
                {
                    case AppException e:
                        // custom application error
                        response.StatusCode = (int)HttpStatusCode.BadRequest;
                        break;
                    case KeyNotFoundException e:
                        // not found error
                        response.StatusCode = (int)HttpStatusCode.NotFound;
                        break;
                    default:
                        // unhandled error
                        response.StatusCode = (int)HttpStatusCode.InternalServerError;
                        break;
                }

                var result = JsonSerializer.Serialize(new { message = error?.Message });
                await response.WriteAsync(result);
            }
        }
    }
}

Custom App Exception

The app exception is a custom exception class used to differentiate between handled and unhandled exceptions. Handled exceptions are ones generated by the application and used to display friendly error messages to the client, for example business logic or validation exceptions caused by incorrect input from the user. Unhandled exceptions are generated by the .NET framework and can be caused by bugs in the application code.

using System;
using System.Globalization;

namespace WebApi.Helpers
{
    // custom exception class for throwing application specific exceptions 
    // that can be caught and handled within the application
    public class AppException : Exception
    {
        public AppException() : base() {}

        public AppException(string message) : base(message) { }

        public AppException(string message, params object[] args) 
            : base(String.Format(CultureInfo.CurrentCulture, message, args))
        {
        }
    }
}

Startup Class

The startup class configures the services available to the ASP.NET Core Dependency Injection (DI) container in the ConfigureServices method, and configures the ASP.NET Core request pipeline for the application in the Configure method.

The global error handler middleware is configured on line 60 by calling app.UseMiddleware<ErrorHandlerMiddleware>();

using AutoMapper;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using System;
using WebApi.Helpers;
using WebApi.Middleware;
using WebApi.Services;

namespace WebApi
{
    public class Startup
    {
        public IConfiguration Configuration { get; }

        public Startup(IConfiguration configuration)
        {
            Configuration = configuration;
        }

        // add services to the DI container
        public void ConfigureServices(IServiceCollection services)
        {
            services.AddDbContext<DataContext>();
            services.AddCors();
            services.AddControllers().AddJsonOptions(x => x.JsonSerializerOptions.IgnoreNullValues = true);
            services.AddAutoMapper(AppDomain.CurrentDomain.GetAssemblies());
            services.AddSwaggerGen();

            // configure strongly typed settings object
            services.Configure<AppSettings>(Configuration.GetSection("AppSettings"));

            // configure DI for application services
            services.AddScoped<IAccountService, AccountService>();
            services.AddScoped<IEmailService, EmailService>();
        }

        // configure the HTTP request pipeline
        public void Configure(IApplicationBuilder app, IWebHostEnvironment env, DataContext context)
        {
            // migrate database changes on startup (includes initial db creation)
            context.Database.Migrate();

            // generated swagger json and swagger ui middleware
            app.UseSwagger();
            app.UseSwaggerUI(x => x.SwaggerEndpoint("/swagger/v1/swagger.json", "ASP.NET Core Sign-up and Verification API"));

            app.UseRouting();

            // global cors policy
            app.UseCors(x => x
                .SetIsOriginAllowed(origin => true)
                .AllowAnyMethod()
                .AllowAnyHeader()
                .AllowCredentials());

            // global error handler
            app.UseMiddleware<ErrorHandlerMiddleware>();

            // custom jwt auth middleware
            app.UseMiddleware<JwtMiddleware>();

            app.UseEndpoints(x => x.MapControllers());
        }
    }
}

Example Error Service

This is an example service to show how to throw custom app exceptions and not found exceptions that will be handled by the global error handler middleware.

using System.Collections.Generic;
using WebApi.Helpers;

namespace WebApi.Services
{
    public class ExampleErrorService
    {
        public void ExampleErrors() {
            // a custom app exception that will return a 400 response
            throw new AppException("Email or password is incorrect");

            // a key not found exception that will return a 404 response
            throw new KeyNotFoundException("Account not found");
        }
    }
}

Subscribe to my YouTube channel or follow me on Twitter, Facebook or GitHub to be notified when I post new content.

I’m currently attempting to travel around Australia by motorcycle with my wife Tina on a pair of Royal Enfield Himalayans. You can follow our adventures on YouTube, Instagram and Facebook.

In this article, we will learn about Global Exception Handling in ASP.NET Core applications. Exceptions are something inevitable in any application however well the codebase is. This can usually occur due to external factors as well, like network issues and so on. If these exceptions are not handled well within the application, it may even lead the entire application to terminations and data loss.

The source code for the implementation can be found here.

Getting started with Exception Handling in ASP.NET Core

For this demonstration, We will be working on a new ASP.NET Core Web API Project. I will be using Visual Studio 2019 as my default IDE.

The try-catch block is our go-to approach when it comes to quick exception handling. Let’s see a code snippet that demonstrates the same.

[HttpGet]
public IActionResult Get()
{
    try
    {
        var data = GetData(); //Assume you get some data here which is also likely to throw an exception in certain cases.
        return Ok(data);
    }
    catch (Exception ex)
    {
        _logger.LogError(ex.Message);
        return StatusCode(500);
    }      
}

Here is a basic implementation that we are all used to, yeah? Assume, the method GetData() is a service call that is also prone to exceptions due to certain external factors. The thrown exception is caught by the catch block whose responsibility is to log the error to the console and returns a status code of 500 Internal Server Error in this scenario.

To learn more about logging in ASP.NET Core Applications, I recommend you to go through the following articles that demonstrate Logging with probably the best 2 Logging Frameworks for ASP.NET Core – Serilog & NLog

Let’s say that there was an exception during the execution of the Get() method. The below code is the exception that gets triggered.

throw new Exception("An error occurred...");

Here is what you would be seeing on Swagger.

image Global Exception Handling in ASP.NET Core - Ultimate Guide

The Console may get you a bit more details on the exception, like the line number and other trace logs.

image 1 Global Exception Handling in ASP.NET Core - Ultimate Guide

Although this is a simple way for handling exceptions, this can also increase the lines of code of our application. Yes, you could have this approach for very simple and small applications. Imagine having to write the try-catch block in each and every controller’s action and other service methods. Pretty repetitive and not feasible, yeah?

It would be ideal if there was a way to handle all the exceptions centrally in one location, right? In the next sections, we will see 2 such approaches that can drastically improve our exception handling mechanism by isolating all the handling logics to a single area. This not only gives a better codebase but a more controlled application with even lesser exception handling concerns.

Default Exception Handling Middleware in ASP.NET Core

To make things easier, UseExceptionHandler Middleware comes out of the box with ASP.NET Core applications. This when configured in the Configure method of the startup class adds a middleware to the pipeline of the application that will catch any exceptions in and out of the application. That’s how Middlewares and pipelines work, yeah?

Let’s see how UseExceptionHandler is implemented. Open up the Configure method in the Startup class of your ASP.NET Core application and configure the following.

app.UseExceptionHandler(
    options =>
    {
        options.Run(async context =>
            {
                context.Response.StatusCode = (int)HttpStatusCode.BadRequest;
                context.Response.ContentType = "text/html";
                var exceptionObject = context.Features.Get<IExceptionHandlerFeature>();
                if (null != exceptionObject)
                {
                    var errorMessage = $"{exceptionObject.Error.Message}";
                    await context.Response.WriteAsync(errorMessage).ConfigureAwait(false);
            }});
    }
);

This is a very basic setup & usage of UseExceptionHandler Middleware. So, whenever there is an exception that is detected within the Pipeline of the application, the control falls back to this middleware, which in return will send a custom response to the request sender.

In this case, a status code of 400 Bad Request is sent along with the Message content of the original exception which in our scenario is ‘An error occurred…’. Pretty straight-forward, yeah? Here is how the exception is displayed on Swagger.

image 2 Global Exception Handling in ASP.NET Core - Ultimate Guide

Now, whenever there is an exception thrown in any part of the application, this middleware catches it and throws the required exception back to the consumer. Much cleaned-up code, yeah? But there are still more ways to make this better, by miles.

Custom Middleware – Global Exception Handling In ASP.NET Core

In this section let’s create a Custom Global Exception Handling Middleware that gives even more control to the developer and makes the entire process much better.

Custom Global Exception Handling Middleware – Firstly, what is it? It’s a piece of code that can be configured as a middleware in the ASP.NET Core pipeline which contains our custom error handling logics. There are a variety of exceptions that can be caught by this pipeline.

We will also be creating Custom Exception classes that can essentially make your application throw more sensible exceptions that can be easily understood.

But before that, let’s build a Response class that I recommend to be a part of every project you build, at least the concept. So, the idea is to make your ASP.NET Core API send uniform responses no matter what kind of requests it gets hit with. This make the work easier for whoever is consuming your API. Additionally it gives a much experience while developing.

Create a new class ApiResponse and copy down the following.

public class ApiResponse<T>
{
    public T Data { get; set; }
    public bool Succeeded { get; set; }
    public string Message { get; set; }
    public static ApiResponse<T> Fail(string errorMessage)
    {
        return new ApiResponse<T> { Succeeded = false, Message = errorMessage };
    }
    public static ApiResponse<T> Success(T data)
    {
        return new ApiResponse<T> { Succeeded = true, Data = data };
    }
}

The ApiResponse class is of a generic type, meaning any kind of data can be passed along with it. Data property will hold the actual data returned from the server. Message contains any Exceptions or Info message in string type. And finally there is a boolean that denotes if the request is a success. You can add multiple other properties as well depending on your requirement.

We also have Fail and Success method that is built specifically for our Exception handling scenario. You can find how this is being used in the upcoming sections.

As mentioned earlier, let’s also create a custom exception. Create a new class and name it SomeException.cs or anything. Make sure that you inherit Exception as the base class. Here is how the custom exception looks like.

public class SomeException : Exception
{
    public SomeException() : base()
    {
    }
    public SomeException(string message) : base(message)
    {
    }
    public SomeException(string message, params object[] args) : base(String.Format(CultureInfo.CurrentCulture, message, args))
    {
    }
}

Here is how you would be using this Custom Exception class that we created now.

throw new SomeException("An error occurred...");

Get the idea, right? In this way you can actually differentiate between exceptions. To get even more clarity related to this scenario, let’s say we have other custom exceptions like ProductNotFoundException , StockExpiredException, CustomerInvalidException and so on. Just give some meaningful names so that you can easily identify. Now you can use these exception classes wherever the specific exception arises. This sends the related exception to the middleware, which has logics to handle it.

Now, let’s create the Global Exception Handling Middleware. Create a new class and name it ErrorHandlerMiddleware.cs

public class ErrorHandlerMiddleware
{
    private readonly RequestDelegate _next;
    public ErrorHandlerMiddleware(RequestDelegate next)
    {
        _next = next;
    }
    public async Task Invoke(HttpContext context)
    {
        try
        {
            await _next(context);
        }
        catch (Exception error)
        {
            var response = context.Response;
            response.ContentType = "application/json";
            var responseModel = ApiResponse<string>.Fail(error.Message);
            switch (error)
            {
                case SomeException e:
                    // custom application error
                    response.StatusCode = (int)HttpStatusCode.BadRequest;
                    break;
                case KeyNotFoundException e:
                    // not found error
                    response.StatusCode = (int)HttpStatusCode.NotFound;
                    break;
                default:
                    // unhandled error
                    response.StatusCode = (int)HttpStatusCode.InternalServerError;
                    break;
            }
            var result = JsonSerializer.Serialize(responseModel);
            await response.WriteAsync(result);
        }
    }
}

Line 3 – RequestDelegate denotes a HTTP Request completion.
Line 10 – A simple try-catch block over the request delegate. It means that whenever there is an exception of any type in the pipeline for the current request, control goes to the catch block. In this middleware, Catch block has all the goodness.

Line 14 – Catches all the Exceptions. Remember, all our custom exceptions are derived from the Exception base class.
Line 18 – Creates an APIReponse Model out of the error message using the Fail method that we created earlier.
Line 21 – In case the caught exception is of type SomeException, the status code is set to BadRequest. You get the idea, yeah? The other exceptions are also handled in a similar fashion.
Line 34 – Finally, the created api-response model is serialized and send as a response.

Before running this implementation, make sure that you don’t miss adding this middleware to the application pipeline. Open up the Startup.cs / Configure method and add in the following line.

app.UseMiddleware<ErrorHandlerMiddleware>();

Make sure that you comment out or delete the UseExceptionHandler default middleware as it may cause unwanted clashes. It doesn’t make sense to have multiple middlewares doing the same thing, yeah?

I also assume that you have done the necessary changes that will throw the SomeException Exception in the Get method of the default controller you are working with.

With that done, let’s run the application and see how the error get’s displayed on Swagger.

image 3 Global Exception Handling in ASP.NET Core - Ultimate Guide

There you go! You can see how well built the response is and how easy it is to read what the API has to say to the client. Now, we have a completely custom-built error handling mechanism, all in one place. And yes, of course as mentioned earlier, you are always free to add more properties to the API Reponses class that suits your application’s needs.

I have been using this approach for literally all of my open source projects, and it’s With that, let’s wrap up the article for now 😉

Consider supporting me by buying me a coffee.

Thank you for visiting. You can buy me a coffee by clicking the button below. Cheers!

Buy Me A Coffee

Summary

In this article, we have looked through various ways to implement Exception handling in our ASP.NET Core applications. The favorite approach should definitely be the one where we implemented Global Exception Handling in ASP.NET Core using Custom Middlewares. You can also find the complete source code on my Github here. Have any suggestions or questions? Feel free to leave them in the comments section below. Thanks and Happy Coding! 😀

I have some basic code to determine errors in my MVC application. Currently in my project I have a controller called Error with action methods HTTPError404(), HTTPError500(), and General(). They all accept a string parameter error. Using or modifying the code below.
What is the best/proper way to pass the data to the Error controller for processing? I would like to have a solution as robust as possible.

protected void Application_Error(object sender, EventArgs e)
{
    Exception exception = Server.GetLastError();
    Response.Clear();

    HttpException httpException = exception as HttpException;
    if (httpException != null)
    {
        RouteData routeData = new RouteData();
        routeData.Values.Add("controller", "Error");
        switch (httpException.GetHttpCode())
        {
            case 404:
                // page not found
                routeData.Values.Add("action", "HttpError404");
                break;
            case 500:
                // server error
                routeData.Values.Add("action", "HttpError500");
                break;
            default:
                routeData.Values.Add("action", "General");
                break;
        }
        routeData.Values.Add("error", exception);
        // clear error on server
        Server.ClearError();

        // at this point how to properly pass route data to error controller?
    }
}

Some developer's user avatar

asked Jul 23, 2009 at 11:07

aherrick's user avatar

Instead of creating a new route for that, you could just redirect to your controller/action and pass the information via querystring. For instance:

protected void Application_Error(object sender, EventArgs e) {
  Exception exception = Server.GetLastError();
  Response.Clear();

  HttpException httpException = exception as HttpException;

  if (httpException != null) {
    string action;

    switch (httpException.GetHttpCode()) {
      case 404:
        // page not found
        action = "HttpError404";
        break;
      case 500:
        // server error
        action = "HttpError500";
        break;
      default:
        action = "General";
        break;
      }

      // clear error on server
      Server.ClearError();

      Response.Redirect(String.Format("~/Error/{0}/?message={1}", action, exception.Message));
    }

Then your controller will receive whatever you want:

// GET: /Error/HttpError404
public ActionResult HttpError404(string message) {
   return View("SomeView", message);
}

There are some tradeoffs with your approach. Be very very careful with looping in this kind of error handling. Other thing is that since you are going through the asp.net pipeline to handle a 404, you will create a session object for all those hits. This can be an issue (performance) for heavily used systems.

Tim Cooper's user avatar

Tim Cooper

156k38 gold badges325 silver badges276 bronze badges

answered Jul 23, 2009 at 13:40

andrecarlucci's user avatar

andrecarlucciandrecarlucci

5,7877 gold badges52 silver badges56 bronze badges

9

To answer the initial question «how to properly pass routedata to error controller?»:

IController errorController = new ErrorController();
errorController.Execute(new RequestContext(new HttpContextWrapper(Context), routeData));

Then in your ErrorController class, implement a function like this:

[AcceptVerbs(HttpVerbs.Get)]
public ViewResult Error(Exception exception)
{
    return View("Error", exception);
}

This pushes the exception into the View. The view page should be declared as follows:

<%@ Page Language="C#" Inherits="System.Web.Mvc.ViewPage<System.Exception>" %>

And the code to display the error:

<% if(Model != null) { %>  <p><b>Detailed error:</b><br />  <span class="error"><%= Helpers.General.GetErrorMessage((Exception)Model, false) %></span></p> <% } %>

Here is the function that gathers the all exception messages from the exception tree:

    public static string GetErrorMessage(Exception ex, bool includeStackTrace)
    {
        StringBuilder msg = new StringBuilder();
        BuildErrorMessage(ex, ref msg);
        if (includeStackTrace)
        {
            msg.Append("n");
            msg.Append(ex.StackTrace);
        }
        return msg.ToString();
    }

    private static void BuildErrorMessage(Exception ex, ref StringBuilder msg)
    {
        if (ex != null)
        {
            msg.Append(ex.Message);
            msg.Append("n");
            if (ex.InnerException != null)
            {
                BuildErrorMessage(ex.InnerException, ref msg);
            }
        }
    }

Tim Cooper's user avatar

Tim Cooper

156k38 gold badges325 silver badges276 bronze badges

answered Jan 12, 2010 at 13:55

I found a solution for ajax issue noted by Lion_cl.

global.asax:

protected void Application_Error()
    {           
        if (HttpContext.Current.Request.IsAjaxRequest())
        {
            HttpContext ctx = HttpContext.Current;
            ctx.Response.Clear();
            RequestContext rc = ((MvcHandler)ctx.CurrentHandler).RequestContext;
            rc.RouteData.Values["action"] = "AjaxGlobalError";

            // TODO: distinguish between 404 and other errors if needed
            rc.RouteData.Values["newActionName"] = "WrongRequest";

            rc.RouteData.Values["controller"] = "ErrorPages";
            IControllerFactory factory = ControllerBuilder.Current.GetControllerFactory();
            IController controller = factory.CreateController(rc, "ErrorPages");
            controller.Execute(rc);
            ctx.Server.ClearError();
        }
    }

ErrorPagesController

public ActionResult AjaxGlobalError(string newActionName)
    {
        return new AjaxRedirectResult(Url.Action(newActionName), this.ControllerContext);
    }

AjaxRedirectResult

public class AjaxRedirectResult : RedirectResult
{
    public AjaxRedirectResult(string url, ControllerContext controllerContext)
        : base(url)
    {
        ExecuteResult(controllerContext);
    }

    public override void ExecuteResult(ControllerContext context)
    {
        if (context.RequestContext.HttpContext.Request.IsAjaxRequest())
        {
            JavaScriptResult result = new JavaScriptResult()
            {
                Script = "try{history.pushState(null,null,window.location.href);}catch(err){}window.location.replace('" + UrlHelper.GenerateContentUrl(this.Url, context.HttpContext) + "');"
            };

            result.ExecuteResult(context);
        }
        else
        {
            base.ExecuteResult(context);
        }
    }
}

AjaxRequestExtension

public static class AjaxRequestExtension
{
    public static bool IsAjaxRequest(this HttpRequest request)
    {
        return (request.Headers["X-Requested-With"] != null && request.Headers["X-Requested-With"] == "XMLHttpRequest");
    }
}

1

I struggled with the idea of centralizing a global error handling routine in an MVC app before. I have a post on the ASP.NET forums.

It basically handles all your application errors in the global.asax without the need for an error controller, decorating with the [HandlerError] attribute, or fiddling with the customErrors node in the web.config.

p.campbell's user avatar

p.campbell

97.4k67 gold badges255 silver badges319 bronze badges

answered Jan 22, 2010 at 13:47

Jack Hsu's user avatar

Jack HsuJack Hsu

991 silver badge1 bronze badge

Perhaps a better way of handling errors in MVC is to apply the HandleError attribute to your controller or action and update the Shared/Error.aspx file to do what you want. The Model object on that page includes an Exception property as well as ControllerName and ActionName.

answered Aug 12, 2009 at 21:30

Brian's user avatar

BrianBrian

37k22 gold badges93 silver badges109 bronze badges

3

This may not be the best way for MVC ( https://stackoverflow.com/a/9461386/5869805 )

Below is how you render a view in Application_Error and write it to http response. You do not need to use redirect. This will prevent a second request to server, so the link in browser’s address bar will stay same. This may be good or bad, it depends on what you want.

Global.asax.cs

protected void Application_Error()
{
    var exception = Server.GetLastError();
    // TODO do whatever you want with exception, such as logging, set errorMessage, etc.
    var errorMessage = "SOME FRIENDLY MESSAGE";

    // TODO: UPDATE BELOW FOUR PARAMETERS ACCORDING TO YOUR ERROR HANDLING ACTION
    var errorArea = "AREA";
    var errorController = "CONTROLLER";
    var errorAction = "ACTION";
    var pathToViewFile = $"~/Areas/{errorArea}/Views/{errorController}/{errorAction}.cshtml"; // THIS SHOULD BE THE PATH IN FILESYSTEM RELATIVE TO WHERE YOUR CSPROJ FILE IS!

    var requestControllerName = Convert.ToString(HttpContext.Current.Request.RequestContext?.RouteData?.Values["controller"]);
    var requestActionName = Convert.ToString(HttpContext.Current.Request.RequestContext?.RouteData?.Values["action"]);

    var controller = new BaseController(); // REPLACE THIS WITH YOUR BASE CONTROLLER CLASS
    var routeData = new RouteData { DataTokens = { { "area", errorArea } }, Values = { { "controller", errorController }, {"action", errorAction} } };
    var controllerContext = new ControllerContext(new HttpContextWrapper(HttpContext.Current), routeData, controller);
    controller.ControllerContext = controllerContext;

    var sw = new StringWriter();
    var razorView = new RazorView(controller.ControllerContext, pathToViewFile, "", false, null);
    var model = new ViewDataDictionary(new HandleErrorInfo(exception, requestControllerName, requestActionName));
    var viewContext = new ViewContext(controller.ControllerContext, razorView, model, new TempDataDictionary(), sw);
    viewContext.ViewBag.ErrorMessage = errorMessage;
    //TODO: add to ViewBag what you need
    razorView.Render(viewContext, sw);
    HttpContext.Current.Response.Write(sw);
    Server.ClearError();
    HttpContext.Current.Response.End(); // No more processing needed (ex: by default controller/action routing), flush the response out and raise EndRequest event.
}

View

@model HandleErrorInfo
@{
    ViewBag.Title = "Error";
    // TODO: SET YOUR LAYOUT
}
<div class="">
    ViewBag.ErrorMessage
</div>
@if(Model != null && HttpContext.Current.IsDebuggingEnabled)
{
    <div class="" style="background:khaki">
        <p>
            <b>Exception:</b> @Model.Exception.Message <br/>
            <b>Controller:</b> @Model.ControllerName <br/>
            <b>Action:</b> @Model.ActionName <br/>
        </p>
        <div>
            <pre>
                @Model.Exception.StackTrace
            </pre>
        </div>
    </div>
}

Community's user avatar

answered Sep 28, 2016 at 13:58

burkay's user avatar

burkayburkay

1,0281 gold badge10 silver badges20 bronze badges

4

Application_Error having issue with Ajax requests. If error handled in Action which called by Ajax — it will display your Error View inside the resulting container.

answered Aug 18, 2009 at 21:35

Victor Gelmutdinov's user avatar

Brian,
This approach works great for non-Ajax requests, but as Lion_cl stated, if you have an error during an Ajax call, your Share/Error.aspx view (or your custom error page view) will be returned to the Ajax caller—the user will NOT be redirected to the error page.

answered Dec 13, 2011 at 0:07

undeniablyrob's user avatar

undeniablyrobundeniablyrob

1,3192 gold badges15 silver badges15 bronze badges

Use Following code for redirecting on route page.
Use exception.Message instide of exception. Coz exception query string gives error if it extends the querystring length.

routeData.Values.Add("error", exception.Message);
// clear error on server
Server.ClearError();
Response.RedirectToRoute(routeData.Values);

answered May 11, 2016 at 6:53

Swapnil Malap's user avatar

I have problem with this error handling approach:
In case of web.config:

<customErrors mode="On"/>

The error handler is searching view Error.shtml
and the control flow step in to Application_Error global.asax only after exception

System.InvalidOperationException: The view ‘Error’ or its master was
not found or no view engine supports the searched locations. The
following locations were searched: ~/Views/home/Error.aspx
~/Views/home/Error.ascx ~/Views/Shared/Error.aspx
~/Views/Shared/Error.ascx ~/Views/home/Error.cshtml
~/Views/home/Error.vbhtml ~/Views/Shared/Error.cshtml
~/Views/Shared/Error.vbhtml at
System.Web.Mvc.ViewResult.FindView(ControllerContext context)
………………..

So

 Exception exception = Server.GetLastError();
  Response.Clear();
  HttpException httpException = exception as HttpException;

httpException is always null then
customErrors mode=»On»
:(
It is misleading
Then <customErrors mode="Off"/> or <customErrors mode="RemoteOnly"/> the users see customErrors html,
Then customErrors mode=»On» this code is wrong too


Another problem of this code that

Response.Redirect(String.Format("~/Error/{0}/?message={1}", action, exception.Message));

Return page with code 302 instead real error code(402,403 etc)

answered Aug 24, 2017 at 9:18

Александр Шмыков's user avatar

Posts | About

April 16, 2019 by Areg Sarkissian

Introduction

It is critical to have global exception handling for ASP.NET Core applications to respond appropriately to exceptions that are not handled by application logic.

Global exception handling allows us to log all unhandled exceptions in a central location in our application and then provide a user friendly response to the user.

In ASP.NET MVC projects, there are generally two types of content returned to the web browser. There are normal web page requests that require returning HTML content and there are AJAX requests that normally require returning JSON formatted content.

When the browser requests a HTML rendered page and an exception occurs that the application cannot handle, we generally redirect the browser to an HTML error page.

However, when the browser makes an AJAX request that expects a JSON response then we need to return a JSON error response instead of redirecting to a HTML error page.

Given this, in a global exception handler, we need to distinguish between normal web page requests, so that we can return the appropriate error response.

Using HTTP request headers in the Global Exception Handler

The way we can detect if an AJAX request expects a JSON response is by inspecting the HTTP Accept header sent by the request.

The global exception handler in our MVC application can determine whether to send a JSON error response or redirect to a HTML error page based on the value of the Accept header.

If an Accept header exists that contains the value application/json, then the handler needs to respond with a JSON error response.

Note: Detecting if a request is an AJAX request is not the same as detecting whether the request accepts a JSON response. To detect an AJAX request you can check for a X-Requested-With request header that contains the value xmlhttprequest.

Adding a Global exception handler middleware

We can add a Global exception handler middleware that can access the unhandled exception and the request headers from the HTTP request context to decide how to format the exception data for the response.

This middleware will return JSON data for requests that contain the applicationjson Accept header and otherwise will redirect to an HTML error page.

Also the middleware will serialize and log the exception information.

Below you can see my sample implementation of the global exception handler middleware implemented as an IApplicationBuilder extension method:

public static class GlobalExceptionHandlerExtension
{
    //This method will globally handle logging unhandled execeptions.
    //It will respond json response for ajax calls that send the json accept header
    //otherwise it will redirect to an error page
    public static void UseGlobalExceptionHandler(this IApplicationBuilder app
                                                , ILogger logger
                                                , string errorPagePath
                                                , bool respondWithJsonErrorDetails=false)
    {
        app.UseExceptionHandler(appBuilder =>
        {
            appBuilder.Run(async context =>
            {
                //============================================================
                //Log Exception
                //============================================================
                var exception = context.Features.Get<IExceptionHandlerFeature>().Error;

                string errorDetails = $@"{exception.Message}
                                         {Environment.NewLine}
                                         {exception.StackTrace}";

                int statusCode = (int)HttpStatusCode.InternalServerError;

                context.Response.StatusCode = statusCode;

                var problemDetails = new ProblemDetails
                {
                    Title = "Unexpected Error",
                    Status = statusCode,
                    Detail = errorDetails,
                    Instance = Guid.NewGuid().ToString()
                };

                var json = JsonConvert.SerializeObject(problemDetails);

                logger.LogError(json);

                //============================================================
                //Return response
                //============================================================
                var matchText="JSON";

                bool requiresJsonResponse = context.Request
                                                    .GetTypedHeaders()
                                                    .Accept
                                                    .Any(t => t.Suffix.Value?.ToUpper() == matchText
                                                              || t.SubTypeWithoutSuffix.Value?.ToUpper() == matchText);

                if (requiresJsonResponse)
                {
                    context.Response.ContentType = "application/json; charset=utf-8";

                    if(!respondWithJsonErrorDetails)
                        json = JsonConvert.SerializeObject(new { Title = "Unexpected Error"
                                                               , Status = statusCode});
                    await context.Response
                                    .WriteAsync(json, Encoding.UTF8);
                }
                else
                {
                    context.Response.Redirect(errorPagePath);

                    await Task.CompletedTask;
                }
            });
        });
    }
}

ASP.NET Core 2.2 has infrastructure code that makes it easy to parse out the Accept header components and an error data container that can be serialized to JSON and returned as a response.

The handler first logs the error using the supplied logger and then returns the proper response based on the content of the Accept header.

An additional flag is used to limit the JSON data returned in the response.

We can now replace the original app.UseExceptionHandler("/Home/Error") call in the Startup.Configure(...) method with our own exception handler middleware:

public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
    if (env.IsDevelopment())
    {
        app.UseDeveloperExceptionPage();
    }
    else
    {
        app.UseGlobalExceptionHandler( _logger
                                    , errorPagePath: "/Home/Error"
                                    , respondWithJsonErrorDetails: true);

        //Replaced UseExceptionHandler with UseGlobalExceptionHandler
        //app.UseExceptionHandler("/Home/Error");

        app.UseHsts();
    }

    app.UseHttpsRedirection();
    app.UseStaticFiles();
    app.UseCookiePolicy();

    app.UseMvc(routes =>
    {
        routes.MapRoute(
            name: "default",
            template: "{controller=Home}/{action=Index}/{id?}");
    });
}

The complete source code of the demo app can be found in my Github https://github.com/aregsar/mvcapp.

Testing the Global Exception Handler middleware

To do a quick test of the installed the global exception handler middleware, I added an Ajax action method to the HomeController class. The method normally returns JSON data but throws an exception if the route contains the optional id parameter.

I also modified the Privacy action method that normally returns the Privacy view but throws an exception if the route contains the id parameter.

Here is the code for these action methods:

public IActionResult Privacy(int? id)
{
    if(id.HasValue)
        throw new Exception("privacy page exception");

    return View();
}

public IActionResult Ajax(int? id)
{
    if(id.HasValue)
        throw new Exception("ajax exception");

    return Json(new {name="ajax"});
}

Now, looking in the Startup.Configure(...) method we can see that the global exception handler middleware is installed in the middleware pipeline only for production builds.

Therefore, to run the server in production mode, I added a Production run configuration profile in the profiles section of the propertieslaunchSettings.json file.

Here is the production configuration from the launchSettings file:

 "prod": {
      "commandName": "Project",
      "launchBrowser": true,
      "applicationUrl": "https://localhost:5001;http://localhost:5000",
      "environmentVariables": {
        "ASPNETCORE_ENVIRONMENT": "Production"
      }

After adding the configuration profile we can use the dotnet run command with the –launch-profile option to run the app in production mode.

dotnet run --launch-profile prod

We can now quickly test the global exception handling middleware code by issuing curl commands against the Ajax action endpoint.

To do so we can open a new terminal tab and curl a request to the https://localhost:5001/home/ajax URL:

curl -i -H "Accept: application/json" https://localhost:5001/home/ajax
HTTP/1.1 200 OK
Date: Thu, 11 Apr 2019 22:47:56 GMT
Content-Type: application/json; charset=utf-8
Server: Kestrel
Transfer-Encoding: chunked

{"name":"ajax"}

We can see the normal JSON response of the Ajax action method since there was no unhandled exception.

Next we can add an id parameter to the URL to activate the exception in the Ajax action method:

curl -i -H "Accept: application/json" https://localhost:5001/home/ajax/1
HTTP/1.1 500 Internal Server Error
Date: Thu, 11 Apr 2019 22:46:27 GMT
Content-Type: application/json
Server: Kestrel
Cache-Control: no-cache
Pragma: no-cache
Transfer-Encoding: chunked
Expires: -1

{
  "title": "Unexpected error",
  "status": 500,
  "detail": "test exceptionrn                                             nrn                                                at mvcapp.Controllers.HomeController.ajax(Nullable`1 id) in /Users/aregsarkissian/projects/asp3/mvcapp/Controllers/HomeController.cs:line 29n   at lambda_method(Closure , Object , Object[] )n   at Microsoft.Extensions.Internal.ObjectMethodExecutor.Execute(Object target, Object[] parameters)n   at Microsoft.AspNetCore.Mvc.Internal.ActionMethodExecutor.SyncActionResultExecutor.Execute(IActionResultTypeMapper mapper, ObjectMethodExecutor executor, Object controller, Object[] arguments)n   at Microsoft.AspNetCore.Mvc.Internal.ControllerActionInvoker.InvokeActionMethodAsync()n   at Microsoft.AspNetCore.Mvc.Internal.ControllerActionInvoker.InvokeNextActionFilterAsync()n   at Microsoft.AspNetCore.Mvc.Internal.ControllerActionInvoker.Rethrow(ActionExecutedContext context)n   at Microsoft.AspNetCore.Mvc.Internal.ControllerActionInvoker.Next(State& next, Scope& scope, Object& state, Boolean& isCompleted)n   at Microsoft.AspNetCore.Mvc.Internal.ControllerActionInvoker.InvokeInnerFilterAsync()n   at Microsoft.AspNetCore.Mvc.Internal.ResourceInvoker.InvokeNextResourceFilter()n   at Microsoft.AspNetCore.Mvc.Internal.ResourceInvoker.Rethrow(ResourceExecutedContext context)n   at Microsoft.AspNetCore.Mvc.Internal.ResourceInvoker.Next(State& next, Scope& scope, Object& state, Boolean& isCompleted)n   at Microsoft.AspNetCore.Mvc.Internal.ResourceInvoker.InvokeFilterPipelineAsync()n   at Microsoft.AspNetCore.Mvc.Internal.ResourceInvoker.InvokeAsync()n   at Microsoft.AspNetCore.Routing.EndpointMiddleware.Invoke(HttpContext httpContext)n   at Microsoft.AspNetCore.Routing.EndpointRoutingMiddleware.Invoke(HttpContext httpContext)n   at Microsoft.AspNetCore.StaticFiles.StaticFileMiddleware.Invoke(HttpContext context)n   at Microsoft.AspNetCore.Diagnostics.ExceptionHandlerMiddleware.Invoke(HttpContext context)",
  "instance": "9f238f4e-97b4-478d-9ee3-96e91cb1a93c"
}

This time we can see that the response includes the JSON formatted exception data returned by the global exception handler.

Also if we look at the terminal tab where we ran the application, we can see that the unhandled exception is logged as JSON data to the console by the global exception handler.

Next we can quickly test the global exception handler when sending requests to the Privacy action method, by using the web browser to navigate to the Privacy endpoint URL:

https://localhost:5001/home/privacy

We can see the normal privacy HTML page response displayed in the browser.

Finally we can add the id parameter to the Privacy endpoint URL and navigate to the URL to activate the exception in the Privacy action method:

https://localhost:5001/home/privacy/1

This time we can see that we are redirected to a server error page as usual for HTML endpoints that throw unhandled exceptions.

Conclusion

It is easy to add a global exception handling middleware to ASP.NET Core MVC applications to perform custom exception handling.

ASP.NET Core gives us all the facilities that we need to access information in the request headers and the exception data to make our own decision on how we want to respond to unhandled application errors.

Thanks for reading

The features of exception handling help us deal with the unexpected mistakes that can occur in our code. We will use the try-catch block in our code and finally keyword to clean up resources afterward to manage exceptions.

Even though there is nothing wrong with the try-catch blocks in our Actions in Web API project, we can extract all the exception handling logic into a single centralized place. By doing that, we make our actions more readable and the error handling process more maintainable. If we want to make our actions even more readable and maintainable, we can implement Action Filters. We won’t talk about action filters in this post.

In this post, we will first handle errors by using a try-catch block and then rewrite our code to illustrate the advantages of this strategy by using built-in middleware and our custom middleware for global error handling. We are going to use an ASP.NET Core 3.1 Web API project to explain these features.

To download the source code for this project, visit global-exception-handling-aspnetcore

In this post, we are going to talk about:

  • Error Handling with Try-Catch Block
  • Handling errors globally with Built-in Middleware
  • Handling errors globally with Custom Middleware

Error Handling with Try-Catch Block

To start off with an example, let’s open the cities controller from the project (global-exception-handling-aspnetcore). There is one method Get().

using System;
using Microsoft.AspNetCore.Mvc;

namespace GlobalExceptionHandling.Controllers
{
    [Route("api/[controller]")]
    [ApiController]
    public class Cities : ControllerBase
    {
        // GET: api/<Cities>
        [HttpGet]
        public IActionResult Get()
        {
            try
            {
                var cities = DataManager.DataManager.GetCities();

                return Ok(cities);
            }
            catch (Exception)
            {
                return StatusCode(500, "Internal server error");
            }   
        }
    }
}

When we send a request at this endpoint, we will get this result.

Basicrequest.webp

Now let’s modify our code, to force an exception, add the following code right below the GetCities() method.

throw new Exception("Exception while fetching the list of cities.");

Now, let’s send the request again.

Try-Catche-Error.webp

So, this works just fine. But the downside of this approach is that we have to repeat try-catch blocks in all the actions in which we want to handle exceptions. Well, there is a better approach to do that. Let’s talk about that now.

Handling Errors Globally with Built-in Middleware

The UseExceptionHandler middleware is a built-in middleware that we can use to handle exceptions in our ASP.NET Core Web API application.

First, we are going to add the new class ErrorDetails in the Models folder.

using Newtonsoft.Json;

namespace GlobalExceptionHandling.Models
{
    public class ErrorDetails
    {
        public int StatusCode { get; set; }
        public string Message { get; set; }
        public override string ToString()
        {
            return JsonConvert.SerializeObject(this);
        }
    }
}

We will use this class for the details of the error message.

Now, let’s create a new static class ExceptionMiddlewareExtensions.cs in the Extensions folder and paste the below code.

using GlobalExceptionHandling.CustomExceptionMiddleware;
using GlobalExceptionHandling.Models;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Diagnostics;
using Microsoft.AspNetCore.Http;
using System.Net;

namespace GlobalExceptionHandling.Extensions
{
    public static class ExceptionMiddlewareExtensions
    {
        public static void ConfigureExceptionHandler(this IApplicationBuilder app)
        {
            app.UseExceptionHandler(appError =>
            {
                appError.Run(async context =>
                {
                    context.Response.StatusCode = (int)HttpStatusCode.InternalServerError;
                    context.Response.ContentType = "application/json";
                    var contextFeature = context.Features.Get<IExceptionHandlerFeature>();
                    if (contextFeature != null)
                    {
                        await context.Response.WriteAsync(new ErrorDetails()
                        {
                            StatusCode = context.Response.StatusCode,
                            Message = "Internal Server Error."
                        }.ToString());
                    }
                });
            });
        }

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

In the code above, we’ve created an extension method in which we’ve registered the UseExceptionHandler middleware. Then, we’ve populated the status code and the content type of our response, logged the error message, and finally returned the response with the custom-created object.

Now, let’s modify the Configure method in the Startup.cs to use this extension method.

public void ConfigureServices(IServiceCollection services)
        {
            services.AddControllers();
        }
         
        public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
        {
            if (env.IsDevelopment())
            {
                app.UseDeveloperExceptionPage();
            }

            app.ConfigureExceptionHandler();

            app.UseHttpsRedirection();

            app.UseRouting();

            app.UseAuthorization();

            app.UseEndpoints(endpoints =>
            {
                endpoints.MapControllers();
            });
        }

Remove the try-catch block from the cities controller and hit the endpoint to see the result.

using System;
using Microsoft.AspNetCore.Mvc;

namespace GlobalExceptionHandling.Controllers
{
    [Route("api/[controller]")]
    [ApiController]
    public class Cities : ControllerBase
    {
        // GET: api/<Cities>
        [HttpGet]
        public IActionResult Get()
        {
            var cities = DataManager.DataManager.GetCities();

            throw new Exception("Exception while fetching list of cities.");

            return Ok(cities);
        }
    }
}

GlobalHandlerMiddleware.webp

Now, the code is much cleaner, and most importantly, it can be re-used.

Handling errors globally with Custom Middleware

Let’s create ExceptionMiddleware.cs class in the CustomExceptionMiddleware folder and modify that class.

using GlobalExceptionHandling.Models;
using Microsoft.AspNetCore.Http;
using System;
using System.Net;
using System.Threading.Tasks;

namespace GlobalExceptionHandling.CustomExceptionMiddleware
{
    public class ExceptionMiddleware
    {
        private readonly RequestDelegate _next;

        public ExceptionMiddleware(RequestDelegate next)
        {
            _next = next;
        }

        public async Task InvokeAsync(HttpContext httpContext)
        {
            try
            {
                await _next(httpContext);
            }
            catch (Exception ex)
            {

                await HandleExceptionAsync(httpContext, ex);
            }
        }

        private Task HandleExceptionAsync(HttpContext context, Exception ex)
        {
            context.Response.ContentType = "application/json";
            context.Response.StatusCode = (int)HttpStatusCode.InternalServerError;
            return context.Response.WriteAsync(new ErrorDetails()
            {
                StatusCode = context.Response.StatusCode,
                Message = "Internal Server Error from the custom middleware."
            }.ToString());
        }
    }
}

First, we need to register RequestDelegate through dependency injection. The _next parameter of RequestDelegate type is a function delegate that can process our HTTP requests.

Now, let’s create InvokeAsync() method to process RequestDelegate.

HandleExceptionAsync method will trigger the catch block if there is an exception.

Now, let’s add another static method in ExceptionMiddlewareExtensions.cs.

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

Finally, let’s modify Configure method in the Startup class.

public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
        {
            if (env.IsDevelopment())
            {
                app.UseDeveloperExceptionPage();
            }

            //app.ConfigureExceptionHandler();
            app.ConfigureCustomExceptionMiddleware();

            app.UseHttpsRedirection();

            app.UseRouting();

            app.UseAuthorization();

            app.UseEndpoints(endpoints =>
            {
                endpoints.MapControllers();
            });
        }

Now, let’s inspect the result again.

GlobalCustomHandlerMiddleware.webp

Thank you for reading this article.

When generating a new MVC app the ExceptionHandler middleware is provided by default. This will catch any unhandled exceptions within your application and allow you to redirect to a specified route if you so choose. But what about other non-success status codes? Errors such as 404s will not be captured by this middleware as no exception was thrown.

To handle these types of errors you will need to use the StatusCodePages middleware. In this post I’m going to cover how to setup an MVC application to handle both exceptions as well as non-success status codes.

Handling Exceptions

I’m going to start here as the majority of the work is already done by the out of the box template. When you create a new MVC application you will get a Startup.cs with a Configure method which looks like this.

public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
    if (env.IsDevelopment())
    {
        app.UseDeveloperExceptionPage();
    }
    else
    {
        app.UseExceptionHandler("/Home/Error");
    }

    app.UseStaticFiles();
    app.UseCookiePolicy();

    app.UseMvc(routes =>
    {
        routes.MapRoute(
            name: "default",
            template: "{controller=Home}/{action=Index}/{id?}");
    });
}

The line that is important here is app.UseExceptionHandler("/Home/Error"). This statement is registering the ExcepionHandler middleware and it is going to direct the user to the /Home/Errors route whenever an unhandled exception occurs.

All I’m going to do is make a small change so the line reads as follows.

app.UseExceptionHandler("/Error/500");

With that small change in place the next thing I’m going to do is add a new controller, ErrorsController, which is going to handle all the errors from the application.

public class ErrorsController : Controller
{
    [Route("Error/500")]
    public IActionResult Error500()
    {
        return View();
    }
}

NOTE: Do not add HTTP method attributes to the error action method. Using explicit verbs can stop some errors reaching the method.

I’m using attribute routing here as it’s my personal preference feel free to use the default route templates if you prefer.

Next I want to be able to get some decent information about what went wrong in my application so I can log it or email it or do any other logic I may deem necessary. In order to do this I’m going to add the following line to my Error500 action.
Note: You will also need to import the Microsoft.AspNetCore.Diagnostics namespace.

var exceptionData = HttpContext.Features.Get<IExceptionHandlerPathFeature>();

When the ExceptionHandler middleware runs it sets an item on the Features collection for the request called IExceptionHandlerPathFeature. This is one of 2 features added the other is IExceptionHandlerFeature. They both contain a property called Error which has the details of the Exception. But the IExceptionHandlerPathFeature also contains the path from which the exception was thrown. Based on this I would always recommend using IExceptionHandlerPathFeature.

Now I have some information about what went wrong I want to do something with it. Now for the sake for this post I’m just going to add some details to the ViewBag so I can show them on a view. However in a real application I would most likely want to log them and then show the user a friendlier screen.

[Route("Error/500")]
public IActionResult Error500()
{
    var exceptionFeature = HttpContext.Features.Get<IExceptionHandlerPathFeature>();

    if (exceptionFeature != null)
    {
        ViewBag.ErrorMessage = exceptionFeature.Error.Message;
        ViewBag.RouteOfException = exceptionFeature.Path;
    }

    return View();
}

I can now handle any unhandled exceptions that my application throws, then print out the details. Next I want to deal with non-exception based issues, things such as 404s or any other non-success status code my app may produce.

Non-success Status Codes

The StatusCodePages middleware deals with any status codes returned by the app that are between 400 and 599 and don’t have a body. There are three different extensions for the middleware available.

  • UseStatusCodePages
  • UseStatusCodePagesWithRedirects
  • UseStatusCodePagesWithReExecute

UseStatusCodePages

This is the simplest extension. When this is added to the pipeline any status code produced which matches the criteria above will be intercepted and a simple text response will be returned to the caller. Below is an example of what would be returned if a request was made for a page that didn’t exist.

Status Code: 404; Not Found

While this may have its uses, in reality you are probably going to want to do something a bit more sophisticated.

UseStatusCodePagesWithRedirect

This extension will allow you to configure a user friendly error page rather than just the plain text option above.

This extension and the next are extremely similar in how they work except for one key difference. This will redirect the response to the error page location however, in doing so the original error response is lost. The caller will see a 200 status code from the loading of the error page but not the actual status code which triggered the error.

Now this may not matter to you but it is technically wrong as you will be returning a success status code when there was actually an error. You will have to decided if this is OK for your use case.

UseStatusCodePagesWithReExecute

This is the configuration I will be using, it’s also the one I would suggest is best for most cases. The middleware will pick up any matching status codes being returned and then re-execute the pipeline. So when the user friendly error page is returned the correct error status code is returned as well.

I’m going to add the following line underneath the ExceptionHandler middleware from earlier.

app.UseStatusCodePagesWithReExecute("/Error/{0}");

I’ve used the {0} placeholder when defining my Error route. This will be populated with the status code which triggered the middleware. I can then pick this up in an action on the ErrorsController.

[Route("Error/{statusCode}")]
public IActionResult HandleErrorCode(int statusCode)
{
    var statusCodeData = HttpContext.Features.Get<IStatusCodeReExecuteFeature>();

    switch (statusCode)
    {
        case 404:
            ViewBag.ErrorMessage = "Sorry the page you requested could not be found";
            ViewBag.RouteOfException = statusCodeData.OriginalPath;
            break;
        case 500:
            ViewBag.ErrorMessage = "Sorry something went wrong on the server";
            ViewBag.RouteOfException = statusCodeData.OriginalPath;
            break;
    }

    return View();
}

Much like with the ExceptionHandler middleware the StatusCodePages middleware populates an object to give a bit more information about whats happened. This time I can request a IStatusCodeReExecuteFeature from the current requests Features collection. With this I can then access three properties.

  • OriginalPathBase
  • OriginalPath
  • OriginalQueryString

This allows me access to the route that triggered the status code along with any querystring data that may be relevant. Again for the purposes of this post I am just setting some ViewBag data to pass down to the view. But in a real world application I would be logging this information somewhere.

In the example above I have defined a single action to handle all status codes. But you could quite easily define different actions for different status codes. If, for example, you wanted to have a dedicated endpoint for 404 status codes you could define it as follows:

[Route("Error/404")]
public IActionResult HandlePageNotFound()
{
    ...
}

Wrapping up

In this post I’ve gone over a couple of pieces of middleware you can use for global error handling in your ASP.NET Core MVC applications. As always if you have any questions please ask away in the comments and I will do my best to answer them.

Понравилась статья? Поделить с друзьями:
  • Asp net error 500 internal server error
  • Asp net err ssl protocol error
  • Asp net custom error page
  • Asp net core return internal server error
  • Asp net core cors error