In today’s digital landscape, the demand for robust and reliable web APIs is ever-growing. Whether you’re developing a RESTful service, a microservice, or a full-fledged web application, handling exceptions gracefully is a fundamental aspect of ensuring your API’s stability and usability. After all, the real world is far from perfect, and unexpected errors can occur at any time.
Imagine this scenario: You’ve built a sophisticated ASP.NET Core Web API, and everything seems to be running smoothly. Users are interacting with your application, but suddenly, an unhandled exception disrupts the flow, resulting in a confusing error message or, even worse, a crash. How can you prevent such mishaps and deliver a seamless experience to your users?
Enter global exception handling in ASP.NET Core, a powerful mechanism that allows you to intercept and manage exceptions at a central level within your application. This approach ensures that no matter where an exception originates—be it in your controller actions, middleware, or service layer—it can be caught, processed, and transformed into a clear, consistent error response.
In this blog post, we will delve into the world of global exception handling in ASP.NET Core Web APIs. We’ll explore the concept, its significance, and most importantly, how to implement it effectively in your projects. By the end of this journey, you’ll have the tools and knowledge needed to fortify your APIs against unexpected errors, enhance user experiences, and maintain the reputation of your application’s reliability.
So, let’s embark on this journey of learning how to build resilient ASP.NET Core Web APIs by mastering the art of global exception handling.
Setting Up an ASP.NET Core Web API Project
To get started with learning about global exception handling in ASP.NET Core Web APIs, let’s create a sample project. Follow these steps to set up a new ASP.NET Core Web API project using Visual Studio:
- Launch Visual Studio: Open Visual Studio on your development machine.
- Create a New Project: Navigate to the “File” menu, select “New,” and then choose “Project…” from the dropdown.
- Select the Project Template: In the “Create a new project” window, locate and select the “ASP.NET Core Web API” project template. This template is tailored for building RESTful Web APIs.
- Configure Project Settings: Provide a meaningful name for your project, such as “ExceptionHandlingDemo,” and specify the project’s location. Additionally, choose the target framework version. Typically, ASP.NET Core projects use the latest LTS (Long-Term Support) version of the .NET Core framework for stability and long-term compatibility.
- Optional: Authentication and Authorization: Depending on your project’s security requirements, you can configure authentication and authorization settings during project creation. For instance, you might opt to implement JWT (JSON Web Tokens) authentication to secure your API endpoints.
- Create the Project: Click the “Create” button to initiate the creation of your ASP.NET Core Web API project with the specified settings.
Adding Exception Handling to the WeatherForecastController
Now that we have our ASP.NET Core Web API project set up, let’s focus on enhancing the error-handling capabilities of our API by implementing exception handling within the “WeatherForecastController.” Additionally, we’ll take advantage of the async-await pattern to improve responsiveness and scalability.
Async-Await in ASP.NET Core Web APIs
In modern ASP.NET Core Web APIs, asynchronous programming using async
and await
is essential for handling I/O-bound operations efficiently. The async-await pattern allows your API to remain responsive while waiting for time-consuming tasks to complete, such as accessing a database or making external API requests.
Updated GET Method
Here’s an updated version of the GET
method within the “WeatherForecastController” that incorporates async-await:
[HttpGet(Name = "GetWeatherForecast")]
public async Task<ActionResult<IEnumerable<WeatherForecast>>> Get()
{
try
{
return await Task.WhenAll(
Enumerable.Range(1, 5).Select(async index => new WeatherForecast
{
Date = DateOnly.FromDateTime(DateTime.Now.AddDays(index)),
TemperatureC = await Task.Run(() => Random.Shared.Next(-20, 55)), // Simulate async operation
Summary = Summaries[Random.Shared.Next(Summaries.Length)]
})
);
}
catch (Exception ex)
{
// Log the exception (you can use a logging framework like Serilog or NLog)
Console.WriteLine($"An error occurred: {ex}");
return StatusCode(StatusCodes.Status500InternalServerError, "something went wrong");
}
}
In this code:
- We continue to use the
async
andawait
keywords to ensure non-blocking execution, especially when performing potentially time-consuming operations. - The
Task.WhenAll
method is used to asynchronously generate weather forecasts. We generate forecasts for the next 5 days, simulating this as an asynchronous operation.
Exception Handling
Exception handling remains a pivotal aspect of our API’s reliability. Here’s how we handle exceptions:
- Inside the
try
block, we attempt to generate weather forecasts. If an exception occurs during this process, it’s caught by thecatch
block. - In the
catch
block, we log the exception usingConsole.WriteLine
. In a production environment, it’s recommended to use a dedicated logging framework like Serilog or NLog for comprehensive error logging. - We return a
500 Internal Server Error
status code (StatusCodes.Status500InternalServerError
) along with a user-friendly error message, “Something went wrong.” This provides meaningful feedback to the client while not exposing sensitive error details.
Now run the project and execute on swagger, you should see similar output as below
let’s introduce intentional errors into the try
block of the Get
method to simulate real-world scenarios where errors can occur. Here’s the updated code with simulated errors:
try
{
throw new Exception("Some error occured!");
return await Task.WhenAll(
Enumerable.Range(1, 5).Select(async index => new WeatherForecast
{
Date = DateOnly.FromDateTime(DateTime.Now.AddDays(index)),
TemperatureC = await Task.Run(() => Random.Shared.Next(-20, 55)), // Simulate async operation
Summary = Summaries[Random.Shared.Next(Summaries.Length)]
})
);
}
Next run the application and call the same api, you should see output as below.
here you can see we are getting the response 500 and the message which we set in catch block. The actual exception is logged in console as below.
In the code provided, we’re returning a 500 Internal Server Error
response to the client along with a user-friendly error message, “Something went wrong,” while logging the actual exception to the console. This is a common practice in web APIs to provide a meaningful error response to clients while keeping detailed error information for debugging and monitoring purposes.
We can certainly improve our error handling by incorporating unique identifiers for each error and implementing code to log exceptions in databases or log files. These steps are valuable.
However, a significant challenge arises when dealing with multiple APIs. Do we really need to update the code of each API individually and insert try-catch blocks? And what if we need to modify the exception response—would that mean altering the response for every API’s catch block?
These are substantial concerns, and if we answer ‘yes’ to these questions, it could consume a substantial amount of development time. This may not be the most efficient use of our resources. So, what is the solution?
The solution is adding global exception handling. Let us see how we can use it.
Add a new folder “Middleware” and a new class file “ExceptionHandler” in it.
Update the “ExceptionHandler” file code as below.
namespace ExceptionHandlingDemo.Middleware
{
public class ExceptionHandler
{
private readonly ILogger<ExceptionHandler> logger;
private readonly RequestDelegate request;
public ExceptionHandler(ILogger<ExceptionHandler> logger, RequestDelegate request)
{
this.logger = logger;
this.request = request;
}
public async Task InvokeAsync(HttpContext httpContext)
{
try
{
await request(httpContext);
}
catch (Exception ex)
{
// Generate a new GUID to log and send to client so that we able
// to match the error with details exception
var exceptionId = Guid.NewGuid().ToString();
//log the details exception
logger.LogError(ex, "ExceptionId:" + exceptionId, "Message:" + ex.Message);
// return the custom exception to client
httpContext.Response.StatusCode = StatusCodes.Status500InternalServerError;
httpContext.Response.ContentType = "application/json";
await httpContext.Response.WriteAsJsonAsync(new
{
Id = exceptionId,
error = "Some Internal error occured while processing the request"
});
}
}
}
}
The above code demonstrates the creation of custom exception handling middleware for ASP.NET Core applications. This middleware intercepts and handles exceptions that occur during the processing of HTTP requests, ensuring consistent error responses.
Key Points:
- The
ExceptionHandler
class resides in theExceptionHandlingDemo.Middleware
namespace. - It accepts an
ILogger
for logging and aRequestDelegate
to pass the request along. - In the
InvokeAsync
method, exceptions are caught, logged with unique identifiers, and sent as structured JSON responses to clients. - This middleware promotes better error management and a standardized approach to handling exceptions across your ASP.NET Core application.
Now our Exception handling middleware is ready , next we need to inject this so that the framework knows about it.
add below line of code in program.cs file
app.UseMiddleware<ExceptionHandler>();
now comment out or remove try catch handling in the WeatherForecastController GET method. The method should be like below.
The code on line 26 simulates the occurrence of an exception during the processing of an API request. To observe this behavior in action, run the application and then make a request to the API. You should expect the following output:
The response includes an Id
field, which serves as a unique identifier for the error. This identifier can be used to retrieve detailed information about the specific error. Additionally, the code employs a logger to record exception details, which are typically logged to the console by default. Below, you can find an example of the console output, showcasing the logged error information:
As you can observe, the response contains an Id
field, which is a unique identifier for the error. This identifier remains consistent between the logged error details and the response, making it convenient for associating errors.
To further enhance error management, consider extending the logging functionality beyond console output. You can customize the logging mechanism to store error information in a database, text file, or any other suitable storage medium. This approach ensures comprehensive error tracking and facilitates thorough analysis and troubleshooting.
This is how we can implement the Global exception handling in asp.net core Web APIs.
Summary
In this blog post, we’ve explored the implementation of custom exception handling middleware in ASP.NET Core applications. This middleware intercepts exceptions globally, ensuring consistent and user-friendly error responses. Key takeaways include setting up the middleware, handling exceptions, and the flexibility of logging. By adopting such middleware, ASP.NET Core applications can maintain a uniform approach to error management, enhancing overall reliability and maintainability.