Global Error Handling in ASP.NET Core Web API
The features of exception handling help us deal with the unexpected mistakes that can occur in our code. We can remove the try-catch block in our code.
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.
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.
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);
}
}
}
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.
Thank you for reading this article.