Creating a Web API is like to throwing a dinner party for a set of extremely particular people. Even if you have the ideal menu prepared, there will always be that one visitor who is allergic to peanuts, another who demands a gluten-free meal, and still another who simply dislikes anything green.
These unforeseen setbacks are the mistakes your program must manage in the realm of APIs. The database may decide to take a coffee break, a user may submit incorrect data, or, worst of all, an unforeseen error occurs deep within your code. This is where error management comes into play, transforming chaos into a process that can be controlled.
What is error handling, though? Furthermore, what makes global error handling important?
What is Error Handling?
How you handle unforeseen circumstances, or exceptions in technical terms, in your program is known as error handling. Maintaining the reliability of your application and providing a seamless user experience are more important than merely avoiding crashes. Let’s say you are using an app to fill out a form and something goes wrong. Which would you prefer—a clear notice outlining what to do next, or a mysterious error code? That’s right.
Error management gracefully handles these challenges, giving users unambiguous feedback and assisting developers in resolving issues.
Here’s an example of how to use a try-catch block to handle exceptions in a single method:
//See a method wrapped in a try-catch block
public async Task<IActionResult> SignUp(SignUpRequest request)
{
try
{
var result = await _services.UserRegistrationService.NewUser(request);
return StatusCode(result.StatusCode, result);
}
catch(Exception ex)
{
return StatusCode(StatusCodes.Status500InternalServerError, ex.Message);
}
}
In the above example:
The potential exception-throwing code is contained in the try block.
The catch block specifies how to respond to the exception and determine what should happen, such as recording it or giving an error message.
Although this strategy is effective, when used for several functions, it becomes repetitious and clogs the code.
What is Global Error Handling?
Global Error Handling simplifies how exceptions are managed for your entire application through a single, centralized implementation. It provides a “catch-all” system, taking out the need for repetitive try-catch blocks in every single method. This keeps your codebase cleaner and easier to maintain.
With global error handling, your SignUp method could look as simple as this:
//The exceptions are now handled globally, outside the method itself.
public async Task<IActionResult> SignUp(SignUpRequest request)
{
var result = await _services.UserRegistrationService.NewUser(request);
return StatusCode(result.StatusCode, result);
}
Observe how the exceptions are now managed outside of the method itself, globally. Middlewares is used to do this.
Middlewares for Global Error Handling
Middlewares will be used to implement global error handling in the ASP.NET Core Web API. A piece of code or logic in the application’s request-response pipeline is known as middleware. It can change the HTTP answer after it has processed incoming requests before they get to your controllers. Consider them as traffic cops or checkpoints that examine each incoming request, such as error handling or authentication.
You can use one of two kinds of middleware:
- Built-in Middlewares: These are a component of the.NET framework, which is used to handle fundamental functions like routing and managing exceptions with UseExceptionHandler.
- Custom Middlewares: made by you to meet the requirements of a particular application.
Built-in Middleware: UseExceptionHandler
ASP.NET core comes with a built-in middleware called UseExceptionHandler. Although it’s frequently linked to MVC apps, Web APIs can also utilize it to manage global exceptions. It can be set up to return structured JavaScript Object Notation (JSON) replies rather than rerouting to an error page as in MVC.
//Add a new class and implement the built-in UseExceptionHandler method
public static class ExceptionHandlerMiddlewareExtension
{
public static void AddExceptionHandler(this IApplicationBuilder app)
{
app.UseExceptionHandler(error =>
{
error.Run(async context =>
{
context.Response.StatusCode = (int)HttpStatusCode.InternalServerError;
context.Response.ContentType = "application/json";
var contextFeature = context.Features.Get<IExceptionHandlerFeature>();
if (contextFeature != null)
{
string message = "Internal server error";
await context.Response.WriteAsync(JsonConvert.SerializeObject(new ResponseBase<object>(context.Response.StatusCode, message, VarHelper.ResponseStatus.ERROR.ToString())));
}
});
});
}
}
In your program.cs, add it to the pipeline:
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
//Add ExceptionHandler middleware
app.AddExceptionHandler();
// Other middlewares
app.UseRouting();
app.UseCors("AllowAllHeaders");
app.Run();
Custom Middleware(s)
You have total control over how errors are handled when you build your own middleware. Two methods will be discussed in this article: The IMiddleware Interface and the InvokeAsync Method
Using the InvokeAsync Method: This is the traditional approach, which handles requests and responses by using an InvokeAsync method and a RequestDelegate type argument in the constructor.
using Example.Helpers;
using Example.Responses;
using Newtonsoft.Json;
using System.Net;
namespace Example.Middlewares.Exceptions
{
public class ExceptionMiddleware
{
private readonly RequestDelegate _next;
public ExceptionMiddleware(RequestDelegate next)
{
_next = next;
}
public async Task InvokeAsync(HttpContext httpContext)
{
try
{
// Pass the request to the next middleware
await _next(httpContext);
}
catch (Exception ex)
{
await HandleExceptionAsync(httpContext, ex);
}
}
private Task HandleExceptionAsync(HttpContext context, Exception ex)
{
context.Response.StatusCode = (int)HttpStatusCode.InternalServerError;
context.Response.ContentType = "application/json";
return context.Response.WriteAsync(JsonConvert.SerializeObject(new ResponseBase<object>(
context.Response.StatusCode,
ex.Message,
VarHelper.ResponseStatus.ERROR.ToString())));
}
}
}
Register the middleware to the pipeline:
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
// Add the custom middleware to the pipeline
app.UseMiddleware<ExceptionMiddleware>();
//Other middlewares
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseRouting();
app.UseCors("AllowAllHeaders");
app.UseAuthentication();
app.UseAuthorization();
app.MapControllers();
app.Run();
Using the IMiddleware Method: This method is more modular and cleaner thanks to the IMiddleware interface. Due in large part to the fact that this is my favorite approach, the remark above seems prejudiced. However, please use the approach that works best for you and/or your application.
Let’s go!
How to work with it?
- The IMiddleware interface is implemented by you.
- The InvokeAsync function is where you deal with errors.
- Dependency Injection (DI) is used to register the middleware, which is subsequently registered in the pipeline.
See code implementation:
using Example.Helpers;
using Example.Responses;
using Newtonsoft.Json;
using System.Net;
namespace Example.Middlewares.Exceptions
{
public class ExceptionHandlingMiddleware : IMiddleware
{
public async Task InvokeAsync(HttpContext httpContext, RequestDelegate next)
{
try
{
await next(httpContext);
}
catch(Exception ex)
{
await HandleException(httpContext, ex);
}
}
private Task HandleException(HttpContext context, Exception ex)
{
context.Response.StatusCode = (int)HttpStatusCode.InternalServerError;
context.Response.ContentType = "application/json";
return context.Response.WriteAsync(JsonConvert.SerializeObject(new ResponseBase<object>(
context.Response.StatusCode,
ex.Message,
VarHelper.ResponseStatus.ERROR.ToString(),
ex.InnerException?.Message)));
}
}
}
Add the middleware to the pipeline and register it with Dependency Injection (DI):
var builder = WebApplication.CreateBuilder(args);
// Register the middleware with DI
builder.Services.AddTransient<ExceptionHandlingMiddleware>();
var app = builder.Build();
// Add the custom middleware to the pipeline
app.UseMiddleware<ExceptionHandlingMiddleware>();
// Other middlewares
app.UseRouting();
app.UseCors("AllowAllHeaders");
app.Run();
Verdict
Global error handling, in my opinion, is an indication of well-designed software rather than merely a technical requirement. Either the built-in code or a custom middleware can be used, but centralizing exception management not only makes your code clearer, but it also improves the experience for all parties—developers, users, and team members.

Javier is Content Specialist and also .NET developer. He writes helpful guides and articles, assist with other marketing and .NET community work