Use IExceptionHandler to handle errors in ASP.NET Core
No matter how carefully you design your web pages and components there is always a possibility that your application throws an error at runtime. To trap and handle such unforeseen circumstances you can use IExceptionHandler interface introduced in ASP.NET Core 8.
Before we go to IExceptionHandler, let's write some code that throws errors at runtime and see how your web application responds.
Create a new ASP.NET Core MVC project using Visual Studio Code (or Visual Studio) and open its HomeController from the Controllers folder.
Modify the Index() action as shown below:
public IActionResult Index()
{
string a = "100";
string b = "10";
int result = int.Parse(a) / int.Parse(b);
return View(result);
}
Of course, no body would write such code in a real world application. Here, we keep those numbers as strings so that we can deliberately throw FormatException and DivideByZeroException by specifying invalid values.
The Index view simply outputs the result:
@model int
@{
ViewData["Title"] = "Home Page";
}
<h1>Result = @Model</h1>
And here is a sample run of the application:
Now, change one of the numbers to an arbitrary string value (say, abcd). This will throw FormatException at runtime and the browser window will show this:
Now, change the second number to 0 so that this code will throw DivideByZeroException at run time.
We can easily deal with these exceptions by wrapping this code inside a try-catch block. However, we will trap these exceptions using IExceptionHandler so that we know how to use it in our web applications.
The IExceptionHandler interface from Microsoft.AspNetCore.Diagnostics namespace that allows you to implement a centralized or global error handling logic. If you ever worked with ASP.NET Web Forms you will find this mechanism similar to Application_Error event of Global.asax file.
IExceptionHandler contains just a single async method named TryHandleAsync() that has this signature:
public async ValueTask<bool> TryHandleAsync(
HttpContext httpContext,
Exception exception,
CancellationToken cancellationToken)
{
}
As you can see, TryHandleAsync() returns a boolean value indicating whether the exception has been handled (true) or not (false). If you return true, the exception won't be bubbled up further and if you return false it will continue to bubble up in the HTTP pipeline.
Inside the TryHandleAsync() method, you can observe the Exception parameter and handle the error if you want. You can also use the HttpContext to send some response to the client. This response could be HTML or a ProblemDetails object.
You can create more than one implementations of IExceptionHandler. Once created they all need to be registered with DI and wired in the HTTP pipeline using ASP.NET Core's typical Add* and Use* style methods (you will use them later in this article).
Now that you have some idea about the IExceptionHandler interface, let's go ahead and create two implementations -- one that handles FormatException and another that handles DivideByZeroException.
Add a new class called FormatExceptionHandler and implement IExceptionHandler in it as shown below:
public class FormatExceptionHandler :
IExceptionHandler
{
public async ValueTask<bool> TryHandleAsync(
HttpContext httpContext,
Exception exception,
CancellationToken cancellationToken)
{
if(exception is FormatException)
{
await httpContext.Response.WriteAsync
("<h1>Invalid number(s)</h1>",
cancellationToken);
return true;
}
else
{
await httpContext.Response.WriteAsync
("<h1>FormatExceptionHandler :
I don't handle these errors</h1>",
cancellationToken);
return false;
}
}
}
As you can see, inside the TryHandleAsync() implementation we check the type fo Exception. If it is FormatException we output a custom error message in the response and return true indicating that the error has been handled. For any other exception, we set a message in the response and return false indicating that we haven't handled the error in our implementation.
On the similar lines, add DivideByZeroExceptionHandler class as shown below:
public class DivideByZeroExceptionHandler :
IExceptionHandler
{
public async ValueTask<bool> TryHandleAsync(
HttpContext httpContext,
Exception exception,
CancellationToken cancellationToken)
{
if(exception is DivideByZeroException)
{
await httpContext.Response.WriteAsync
("<h1>Can't divide by 0</h1>", cancellationToken);
return true;
}
else
{
await httpContext.Response.WriteAsync
("<h1>DivideByZeroException :
I don't handle these errors</h1>",
cancellationToken);
return false;
}
}
}
This code should look familiar to you because it is quite similar to the previous implementation. The only difference is that this time it handles DivideByZeroException and outputs an error message accordingly.
Once these two implementations are ready, open Program.cs and register them with the DI container as shown below:
builder.Services.AddControllersWithViews();
builder.Services.AddExceptionHandler
<FormatExceptionHandler>();
builder.Services.AddExceptionHandler
<DivideByZeroExceptionHandler>();
builder.Services.AddProblemDetails();
var app = builder.Build();
We used AddExceptionHandler() method to register our exception handler implementations. The AddExceptionHandler() registers the exception handlers in Singleton mode. And they are called in the order you register them here. In this case FormatExceptionHandler will be called first and then DivideByZeroExceptionHandler (if exception isn't handled by FormatExceptionHandler). The AddProblemDetails() registers Problem Details services that are required by the exception handler middleware (more on that later in this article).
Then wire the Exception Handler middleware in the HTTP pipeline as shown below:
app.UseStatusCodePages();
app.UseExceptionHandler();
app.UseHttpsRedirection();
app.UseStaticFiles();
You use UseExceptionHandler() to wire the middleware.
So far so good. Now it's time to run and check the behavior of our exception handlers.
First set variable a and b to 100 and 10 respectively. Run the application to ensure that there is no error and hence no exception handler gets called.
Then set variable a to "abcd" so that our code will throw FormatException. When you run the application you will find that FormatExceptionHandler gets called and the browser shows this message:
Note that since FormatExceptionHandler returned true indicating the error was handled, DivideByZeroExceptionHandler wasn't called.
Now set variable a back to 100 and set the other variable to 0. This will throw DivideByZeroException and the browser will show this :
Recollect that FormatExceptionHandler is registered first in the Program.cs and hence it was called first. It returns false because it is not supposed to handle DivideByZeroException. Therefore, control goes to the second exception handler DivideByZeroExceptionHandler and it sets the error message as shown in the figure.
In the preceding example we sent HTML response to the client because our client was a browser. What if we are creating Web API / minimal API projects? It is recommended that you return the error information to the client using a standard way -- ProblemDetails object. ProblemDetails class is available in the Microsoft.AspNetCore.Mvc namespace.
We will now modify our exception handlers to return a ProblemDetails object rather than HTML markup.
Modify the TryHandleAsync() implementation of FormatExceptionHandler as shown below:
public async ValueTask<bool> TryHandleAsync(
HttpContext httpContext,
Exception exception,
CancellationToken cancellationToken)
{
if(exception is FormatException)
{
var problemDetails = new ProblemDetails
{
Status =
StatusCodes.Status500InternalServerError,
Title = "Invalid number(s)",
Detail = exception.Message
};
await httpContext.Response.WriteAsJsonAsync
(problemDetails, cancellationToken);
return true;
}
else
{
return false;
}
}
This time we return the error information by creating a ProblemDetails object. We set the Status, Title, and Detail properties. The ProblemDetails object is written to the response stream as JSON. This is done using WriteAsJsonAsync() method. In a more real world situation you won't expose the actual error message to the client due to security risks. Here we are doing it just for our testing purpose.
On the same lines, change the DivideByZeroExceptionHandler as shown below:
public async ValueTask<bool> TryHandleAsync(
HttpContext httpContext,
Exception exception,
CancellationToken cancellationToken)
{
if(exception is DivideByZeroException)
{
var problemDetails = new ProblemDetails
{
Status =
StatusCodes.Status500InternalServerError,
Title = "Can't divide by 0",
Detail = exception.Message
};
await httpContext.Response.WriteAsJsonAsync
(problemDetails, cancellationToken);
return true;
}
else
{
return false;
}
}
If you run the application with FormatException you will see this in the browser:
And if you attempt DivideByZeroException then you will get this:
To summarize, IExceptionHandler provides a centralized way to deal with unknown and unanticipated server side errors. Using IExceptionHandler you can write global exception handler that traps global unhandled errors in your web apps and APIs. You can read more details about IExceptionHandler in the official documentation available here.
That's it for now! Keep coding!!