Error handler asp net core

Documentation for ASP.NET Core. Contribute to dotnet/AspNetCore.Docs development by creating an account on GitHub.
title author description monikerRange ms.author ms.custom ms.date uid

Handle errors in ASP.NET Core

rick-anderson

Discover how to handle errors in ASP.NET Core apps.

>= aspnetcore-3.1

riande

mvc

01/18/2023

fundamentals/error-handling

Handle errors in ASP.NET Core

:::moniker range=»>= aspnetcore-7.0″

By Tom Dykstra

This article covers common approaches to handling errors in ASP.NET Core web apps. See xref:web-api/handle-errors for web APIs.

Developer exception page

The Developer Exception Page displays detailed information about unhandled request exceptions. ASP.NET Core apps enable the developer exception page by default when both:

  • Running in the Development environment.
  • App created with the current templates, that is, using WebApplication.CreateBuilder. Apps created using the WebHost.CreateDefaultBuilder must enable the developer exception page by calling app.UseDeveloperExceptionPage in Configure.

The developer exception page runs early in the middleware pipeline, so that it can catch unhandled exceptions thrown in middleware that follows.

Detailed exception information shouldn’t be displayed publicly when the app runs in the Production environment. For more information on configuring environments, see xref:fundamentals/environments.

The Developer Exception Page can include the following information about the exception and the request:

  • Stack trace
  • Query string parameters, if any
  • Cookies, if any
  • Headers

The Developer Exception Page isn’t guaranteed to provide any information. Use Logging for complete error information.

Exception handler page

To configure a custom error handling page for the Production environment, call xref:Microsoft.AspNetCore.Builder.ExceptionHandlerExtensions.UseExceptionHandler%2A. This exception handling middleware:

  • Catches and logs unhandled exceptions.
  • Re-executes the request in an alternate pipeline using the path indicated. The request isn’t re-executed if the response has started. The template-generated code re-executes the request using the /Error path.

[!WARNING]
If the alternate pipeline throws an exception of its own, Exception Handling Middleware rethrows the original exception.

Since this middleware can re-execute the request pipeline:

  • Middlewares need to handle reentrancy with the same request. This normally means either cleaning up their state after calling _next or caching their processing on the HttpContext to avoid redoing it. When dealing with the request body, this either means buffering or caching the results like the Form reader.
  • For the xref:Microsoft.AspNetCore.Builder.ExceptionHandlerExtensions.UseExceptionHandler(Microsoft.AspNetCore.Builder.IApplicationBuilder,System.String) overload that is used in templates, only the request path is modified, and the route data is cleared. Request data such as headers, method, and items are all reused as-is.
  • Scoped services remain the same.

In the following example, xref:Microsoft.AspNetCore.Builder.ExceptionHandlerExtensions.UseExceptionHandler%2A adds the exception handling middleware in non-Development environments:

:::code language=»csharp» source=»error-handling/samples/7.x/ErrorHandlingSample/Program.cs» id=»snippet_UseExceptionHandler» highlight=»3,5″:::

The Razor Pages app template provides an Error page (.cshtml) and xref:Microsoft.AspNetCore.Mvc.RazorPages.PageModel class (ErrorModel) in the Pages folder. For an MVC app, the project template includes an Error action method and an Error view for the Home controller.

The exception handling middleware re-executes the request using the original HTTP method. If an error handler endpoint is restricted to a specific set of HTTP methods, it runs only for those HTTP methods. For example, an MVC controller action that uses the [HttpGet] attribute runs only for GET requests. To ensure that all requests reach the custom error handling page, don’t restrict them to a specific set of HTTP methods.

To handle exceptions differently based on the original HTTP method:

  • For Razor Pages, create multiple handler methods. For example, use OnGet to handle GET exceptions and use OnPost to handle POST exceptions.
  • For MVC, apply HTTP verb attributes to multiple actions. For example, use [HttpGet] to handle GET exceptions and use [HttpPost] to handle POST exceptions.

To allow unauthenticated users to view the custom error handling page, ensure that it supports anonymous access.

Access the exception

Use xref:Microsoft.AspNetCore.Diagnostics.IExceptionHandlerPathFeature to access the exception and the original request path in an error handler. The following example uses IExceptionHandlerPathFeature to get more information about the exception that was thrown:

:::code language=»csharp» source=»error-handling/samples/7.x/ErrorHandlingSample/Pages/Error.cshtml.cs» id=»snippet_Class» highlight=»15-27″:::

[!WARNING]
Do not serve sensitive error information to clients. Serving errors is a security risk.

Exception handler lambda

An alternative to a custom exception handler page is to provide a lambda to xref:Microsoft.AspNetCore.Builder.ExceptionHandlerExtensions.UseExceptionHandler%2A. Using a lambda allows access to the error before returning the response.

The following code uses a lambda for exception handling:

:::code language=»csharp» source=»error-handling/samples/7.x/ErrorHandlingSample/Snippets/Program.cs» id=»snippet_UseExceptionHandlerInline» highlight=»5-29″:::

[!WARNING]
Do not serve sensitive error information to clients. Serving errors is a security risk.

UseStatusCodePages

By default, an ASP.NET Core app doesn’t provide a status code page for HTTP error status codes, such as 404 — Not Found. When the app sets an HTTP 400-599 error status code that doesn’t have a body, it returns the status code and an empty response body. To enable default text-only handlers for common error status codes, call xref:Microsoft.AspNetCore.Builder.StatusCodePagesExtensions.UseStatusCodePages%2A in Program.cs:

:::code language=»csharp» source=»error-handling/samples/7.x/ErrorHandlingSample/Snippets/Program.cs» id=»snippet_UseStatusCodePages» highlight=»9″:::

Call UseStatusCodePages before request handling middleware. For example, call UseStatusCodePages before the Static File Middleware and the Endpoints Middleware.

When UseStatusCodePages isn’t used, navigating to a URL without an endpoint returns a browser-dependent error message indicating the endpoint can’t be found. When UseStatusCodePages is called, the browser returns the following response:

Status Code: 404; Not Found

UseStatusCodePages isn’t typically used in production because it returns a message that isn’t useful to users.

[!NOTE]
The status code pages middleware does not catch exceptions. To provide a custom error handling page, use the exception handler page.

UseStatusCodePages with format string

To customize the response content type and text, use the overload of xref:Microsoft.AspNetCore.Builder.StatusCodePagesExtensions.UseStatusCodePages%2A that takes a content type and format string:

:::code language=»csharp» source=»error-handling/samples/7.x/ErrorHandlingSample/Snippets/Program.cs» id=»snippet_UseStatusCodePagesContent» highlight=»10″:::

In the preceding code, {0} is a placeholder for the error code.

UseStatusCodePages with a format string isn’t typically used in production because it returns a message that isn’t useful to users.

UseStatusCodePages with lambda

To specify custom error-handling and response-writing code, use the overload of xref:Microsoft.AspNetCore.Builder.StatusCodePagesExtensions.UseStatusCodePages%2A that takes a lambda expression:

:::code language=»csharp» source=»error-handling/samples/7.x/ErrorHandlingSample/Snippets/Program.cs» id=»snippet_UseStatusCodePagesInline» highlight=»9-16″:::

UseStatusCodePages with a lambda isn’t typically used in production because it returns a message that isn’t useful to users.

UseStatusCodePagesWithRedirects

The xref:Microsoft.AspNetCore.Builder.StatusCodePagesExtensions.UseStatusCodePagesWithRedirects%2A extension method:

  • Sends a 302 — Found status code to the client.
  • Redirects the client to the error handling endpoint provided in the URL template. The error handling endpoint typically displays error information and returns HTTP 200.

:::code language=»csharp» source=»error-handling/samples/7.x/ErrorHandlingSample/Snippets/Program.cs» id=»snippet_UseStatusCodePagesRedirect» highlight=»9″:::

The URL template can include a {0} placeholder for the status code, as shown in the preceding code. If the URL template starts with ~ (tilde), the ~ is replaced by the app’s PathBase. When specifying an endpoint in the app, create an MVC view or Razor page for the endpoint.

This method is commonly used when the app:

  • Should redirect the client to a different endpoint, usually in cases where a different app processes the error. For web apps, the client’s browser address bar reflects the redirected endpoint.
  • Shouldn’t preserve and return the original status code with the initial redirect response.

UseStatusCodePagesWithReExecute

The xref:Microsoft.AspNetCore.Builder.StatusCodePagesExtensions.UseStatusCodePagesWithReExecute%2A extension method:

  • Generates the response body by re-executing the request pipeline using an alternate path.
  • Does not alter the status code before or after re-executing the pipeline.

The new pipeline execution may alter the response’s status code, as the new pipeline has full control of the status code. If the new pipeline does not alter the status code, the original status code will be sent to the client.

:::code language=»csharp» source=»error-handling/samples/7.x/ErrorHandlingSample/Snippets/Program.cs» id=»snippet_UseStatusCodePagesReExecute» highlight=»9″:::

If an endpoint within the app is specified, create an MVC view or Razor page for the endpoint.

This method is commonly used when the app should:

  • Process the request without redirecting to a different endpoint. For web apps, the client’s browser address bar reflects the originally requested endpoint.
  • Preserve and return the original status code with the response.

The URL template must start with / and may include a placeholder {0} for the status code. To pass the status code as a query-string parameter, pass a second argument into UseStatusCodePagesWithReExecute. For example:

:::code language=»csharp» source=»error-handling/samples/7.x/ErrorHandlingSample/Snippets/Program.cs» id=»snippet_UseStatusCodePagesReExecuteQueryString»:::

The endpoint that processes the error can get the original URL that generated the error, as shown in the following example:

:::code language=»csharp» source=»error-handling/samples/7.x/ErrorHandlingSample/Pages/StatusCode.cshtml.cs» id=»snippet_Class» highlight=»12-21″:::

Since this middleware can re-execute the request pipeline:

  • Middlewares need to handle reentrancy with the same request. This normally means either cleaning up their state after calling _next or caching their processing on the HttpContext to avoid redoing it. When dealing with the request body, this either means buffering or caching the results like the Form reader.
  • Scoped services remain the same.

Disable status code pages

To disable status code pages for an MVC controller or action method, use the [SkipStatusCodePages] attribute.

To disable status code pages for specific requests in a Razor Pages handler method or in an MVC controller, use xref:Microsoft.AspNetCore.Diagnostics.IStatusCodePagesFeature:

:::code language=»csharp» source=»error-handling/samples/7.x/ErrorHandlingSample/Snippets/Pages/Index.cshtml.cs» id=»snippet_OnGet»:::

Exception-handling code

Code in exception handling pages can also throw exceptions. Production error pages should be tested thoroughly and take extra care to avoid throwing exceptions of their own.

Response headers

Once the headers for a response are sent:

  • The app can’t change the response’s status code.
  • Any exception pages or handlers can’t run. The response must be completed or the connection aborted.

Server exception handling

In addition to the exception handling logic in an app, the HTTP server implementation can handle some exceptions. If the server catches an exception before response headers are sent, the server sends a 500 - Internal Server Error response without a response body. If the server catches an exception after response headers are sent, the server closes the connection. Requests that aren’t handled by the app are handled by the server. Any exception that occurs when the server is handling the request is handled by the server’s exception handling. The app’s custom error pages, exception handling middleware, and filters don’t affect this behavior.

Startup exception handling

Only the hosting layer can handle exceptions that take place during app startup. The host can be configured to capture startup errors and capture detailed errors.

The hosting layer can show an error page for a captured startup error only if the error occurs after host address/port binding. If binding fails:

  • The hosting layer logs a critical exception.
  • The dotnet process crashes.
  • No error page is displayed when the HTTP server is Kestrel.

When running on IIS (or Azure App Service) or IIS Express, a 502.5 — Process Failure is returned by the ASP.NET Core Module if the process can’t start. For more information, see xref:test/troubleshoot-azure-iis.

Database error page

The Database developer page exception filter xref:Microsoft.Extensions.DependencyInjection.DatabaseDeveloperPageExceptionFilterServiceExtensions.AddDatabaseDeveloperPageExceptionFilter%2A captures database-related exceptions that can be resolved by using Entity Framework Core migrations. When these exceptions occur, an HTML response is generated with details of possible actions to resolve the issue. This page is enabled only in the Development environment. The following code adds the Database developer page exception filter:

:::code language=»csharp» source=»error-handling/samples/7.x/ErrorHandlingSample/Program.cs» id=»snippet_AddDatabaseDeveloperPageExceptionFilter» highlight=»3″:::

Exception filters

In MVC apps, exception filters can be configured globally or on a per-controller or per-action basis. In Razor Pages apps, they can be configured globally or per page model. These filters handle any unhandled exceptions that occur during the execution of a controller action or another filter. For more information, see xref:mvc/controllers/filters#exception-filters.

Exception filters are useful for trapping exceptions that occur within MVC actions, but they’re not as flexible as the built-in exception handling middleware, xref:Microsoft.AspNetCore.Builder.ExceptionHandlerExtensions.UseExceptionHandler%2A. We recommend using UseExceptionHandler, unless you need to perform error handling differently based on which MVC action is chosen.

Model state errors

For information about how to handle model state errors, see Model binding and Model validation.

Problem details

[!INCLUDE]

The following code configures the app to generate a problem details response for all HTTP client and server error responses that don’t have a body content yet:

:::code language=»csharp» source=»error-handling/samples/7.x/ErrorHandlingSample/Snippets/Program.cs» id=»snippet_AddProblemDetails» highlight=»3″:::

The next section shows how to customize the problem details response body.

Customize problem details

The automatic creation of a ProblemDetails can be customized using any of the following options:

  1. Use ProblemDetailsOptions.CustomizeProblemDetails
  2. Use a custom IProblemDetailsWriter
  3. Call the IProblemDetailsService in a middleware

CustomizeProblemDetails operation

The generated problem details can be customized using xref:Microsoft.AspNetCore.Http.ProblemDetailsOptions.CustomizeProblemDetails, and the customizations are applied to all auto-generated problem details.

The following code uses xref:Microsoft.AspNetCore.Http.ProblemDetailsOptions to set xref:Microsoft.AspNetCore.Http.ProblemDetailsOptions.CustomizeProblemDetails:

:::code language=»csharp» source=»error-handling/samples/7.x/ErrorHandlingSample/Snippets/Program.cs» id=»snippet_CustomizeProblemDetails» highlight=»3-5″:::

For example, an HTTP Status 400 Bad Request endpoint result produces the following problem details response body:

{
  "type": "https://tools.ietf.org/html/rfc9110#section-15.5.1",
  "title": "Bad Request",
  "status": 400,
  "nodeId": "my-machine-name"
}

Custom IProblemDetailsWriter

An xref:Microsoft.AspNetCore.Http.IProblemDetailsWriter implementation can be created for advanced customizations:

:::code language=»csharp» source=»~/../AspNetCore.Docs.Samples/fundamentals/middleware/problem-details-service/SampleProblemDetailsWriter.cs» :::

Problem details from Middleware

An alternative approach to using xref:Microsoft.AspNetCore.Http.ProblemDetailsOptions with xref:Microsoft.AspNetCore.Http.ProblemDetailsOptions.CustomizeProblemDetails is to set the xref:Microsoft.AspNetCore.Http.ProblemDetailsContext.ProblemDetails in middleware. A problem details response can be written by calling IProblemDetailsService.WriteAsync:

:::code language=»csharp» source=»~/../AspNetCore.Docs.Samples/fundamentals/middleware/problem-details-service/Program.cs» id=»snippet_middleware» highlight=»5,19-40″:::

In the preceding code, the minimal API endpoints /divide and /squareroot return the expected custom problem response on error input.

The API controller endpoints return the default problem response on error input, not the custom problem response. The default problem response is returned because the API controller has written to the response stream, Problem details for error status codes, before IProblemDetailsService.WriteAsync is called and the response is not written again.

The following ValuesController returns xref:Microsoft.AspNetCore.Mvc.BadRequestResult, which writes to the response stream and therefore prevents the custom problem response from being returned.

:::code language=»csharp» source=»~/../AspNetCore.Docs.Samples/fundamentals/middleware/problem-details-service/Controllers/ValuesController.cs» id=»snippet» highlight=»9-17,27-35″:::

The following Values3Controller returns ControllerBase.Problem so the expected custom problem result is returned:

:::code language=»csharp» source=»~/../AspNetCore.Docs.Samples/fundamentals/middleware/problem-details-service/Controllers/ValuesController.cs» id=»snippet3″ highlight=»16-21″:::

Produce a ProblemDetails payload for exceptions

Consider the following app:

:::code language=»csharp» source=»~/../AspNetCore.Docs.Samples/fundamentals/middleware/problem-details-service/Program.cs» id=»snippet_apishort» highlight=»4,8″:::

In non-development environments, when an exception occurs, the following is a standard ProblemDetails response that is returned to the client:

{
"type":"https://tools.ietf.org/html/rfc7231#section-6.6.1",
"title":"An error occurred while processing your request.",
"status":500,"traceId":"00-b644<snip>-00"
}

For most apps, the preceding code is all that’s needed for exceptions. However, the following section shows how to get more detailed problem responses.

An alternative to a custom exception handler page is to provide a lambda to xref:Microsoft.AspNetCore.Builder.ExceptionHandlerExtensions.UseExceptionHandler%2A. Using a lambda allows access to the error and writing a problem details response with IProblemDetailsService.WriteAsync:

:::code language=»csharp» source=»~/../AspNetCore.Docs.Samples/fundamentals/middleware/problem-details-service/Program.cs» id=»snippet_lambda» :::

[!WARNING]
Do not serve sensitive error information to clients. Serving errors is a security risk.

An alternative approach to generate problem details is to use the third-party NuGet package Hellang.Middleware.ProblemDetails that can be used to map exceptions and client errors to problem details.

Additional resources

  • View or download sample code (how to download)
  • xref:test/troubleshoot-azure-iis
  • xref:host-and-deploy/azure-iis-errors-reference

:::moniker-end

:::moniker range=»= aspnetcore-6.0″

By Tom Dykstra

This article covers common approaches to handling errors in ASP.NET Core web apps. See xref:web-api/handle-errors for web APIs.

Developer exception page

The Developer Exception Page displays detailed information about unhandled request exceptions. ASP.NET Core apps enable the developer exception page by default when both:

  • Running in the Development environment.
  • App created with the current templates, that is, using WebApplication.CreateBuilder. Apps created using the WebHost.CreateDefaultBuilder must enable the developer exception page by calling app.UseDeveloperExceptionPage in Configure.

The developer exception page runs early in the middleware pipeline, so that it can catch unhandled exceptions thrown in middleware that follows.

Detailed exception information shouldn’t be displayed publicly when the app runs in the Production environment. For more information on configuring environments, see xref:fundamentals/environments.

The Developer Exception Page can include the following information about the exception and the request:

  • Stack trace
  • Query string parameters, if any
  • Cookies, if any
  • Headers

The Developer Exception Page isn’t guaranteed to provide any information. Use Logging for complete error information.

Exception handler page

To configure a custom error handling page for the Production environment, call xref:Microsoft.AspNetCore.Builder.ExceptionHandlerExtensions.UseExceptionHandler%2A. This exception handling middleware:

  • Catches and logs unhandled exceptions.
  • Re-executes the request in an alternate pipeline using the path indicated. The request isn’t re-executed if the response has started. The template-generated code re-executes the request using the /Error path.

[!WARNING]
If the alternate pipeline throws an exception of its own, Exception Handling Middleware rethrows the original exception.

In the following example, xref:Microsoft.AspNetCore.Builder.ExceptionHandlerExtensions.UseExceptionHandler%2A adds the exception handling middleware in non-Development environments:

:::code language=»csharp» source=»error-handling/samples/6.x/ErrorHandlingSample/Program.cs» id=»snippet_UseExceptionHandler» highlight=»3,5″:::

The Razor Pages app template provides an Error page (.cshtml) and xref:Microsoft.AspNetCore.Mvc.RazorPages.PageModel class (ErrorModel) in the Pages folder. For an MVC app, the project template includes an Error action method and an Error view for the Home controller.

The exception handling middleware re-executes the request using the original HTTP method. If an error handler endpoint is restricted to a specific set of HTTP methods, it runs only for those HTTP methods. For example, an MVC controller action that uses the [HttpGet] attribute runs only for GET requests. To ensure that all requests reach the custom error handling page, don’t restrict them to a specific set of HTTP methods.

To handle exceptions differently based on the original HTTP method:

  • For Razor Pages, create multiple handler methods. For example, use OnGet to handle GET exceptions and use OnPost to handle POST exceptions.
  • For MVC, apply HTTP verb attributes to multiple actions. For example, use [HttpGet] to handle GET exceptions and use [HttpPost] to handle POST exceptions.

To allow unauthenticated users to view the custom error handling page, ensure that it supports anonymous access.

Access the exception

Use xref:Microsoft.AspNetCore.Diagnostics.IExceptionHandlerPathFeature to access the exception and the original request path in an error handler. The following example uses IExceptionHandlerPathFeature to get more information about the exception that was thrown:

:::code language=»csharp» source=»error-handling/samples/6.x/ErrorHandlingSample/Pages/Error.cshtml.cs» id=»snippet_Class» highlight=»15-27″:::

[!WARNING]
Do not serve sensitive error information to clients. Serving errors is a security risk.

Exception handler lambda

An alternative to a custom exception handler page is to provide a lambda to xref:Microsoft.AspNetCore.Builder.ExceptionHandlerExtensions.UseExceptionHandler%2A. Using a lambda allows access to the error before returning the response.

The following code uses a lambda for exception handling:

:::code language=»csharp» source=»error-handling/samples/6.x/ErrorHandlingSample/Snippets/Program.cs» id=»snippet_UseExceptionHandlerInline» highlight=»5-29″:::

[!WARNING]
Do not serve sensitive error information to clients. Serving errors is a security risk.

UseStatusCodePages

By default, an ASP.NET Core app doesn’t provide a status code page for HTTP error status codes, such as 404 — Not Found. When the app sets an HTTP 400-599 error status code that doesn’t have a body, it returns the status code and an empty response body. To enable default text-only handlers for common error status codes, call xref:Microsoft.AspNetCore.Builder.StatusCodePagesExtensions.UseStatusCodePages%2A in Program.cs:

:::code language=»csharp» source=»error-handling/samples/6.x/ErrorHandlingSample/Snippets/Program.cs» id=»snippet_UseStatusCodePages» highlight=»9″:::

Call UseStatusCodePages before request handling middleware. For example, call UseStatusCodePages before the Static File Middleware and the Endpoints Middleware.

When UseStatusCodePages isn’t used, navigating to a URL without an endpoint returns a browser-dependent error message indicating the endpoint can’t be found. When UseStatusCodePages is called, the browser returns the following response:

Status Code: 404; Not Found

UseStatusCodePages isn’t typically used in production because it returns a message that isn’t useful to users.

[!NOTE]
The status code pages middleware does not catch exceptions. To provide a custom error handling page, use the exception handler page.

UseStatusCodePages with format string

To customize the response content type and text, use the overload of xref:Microsoft.AspNetCore.Builder.StatusCodePagesExtensions.UseStatusCodePages%2A that takes a content type and format string:

:::code language=»csharp» source=»error-handling/samples/6.x/ErrorHandlingSample/Snippets/Program.cs» id=»snippet_UseStatusCodePagesContent» highlight=»10″:::

In the preceding code, {0} is a placeholder for the error code.

UseStatusCodePages with a format string isn’t typically used in production because it returns a message that isn’t useful to users.

UseStatusCodePages with lambda

To specify custom error-handling and response-writing code, use the overload of xref:Microsoft.AspNetCore.Builder.StatusCodePagesExtensions.UseStatusCodePages%2A that takes a lambda expression:

:::code language=»csharp» source=»error-handling/samples/6.x/ErrorHandlingSample/Snippets/Program.cs» id=»snippet_UseStatusCodePagesInline» highlight=»9-16″:::

UseStatusCodePages with a lambda isn’t typically used in production because it returns a message that isn’t useful to users.

UseStatusCodePagesWithRedirects

The xref:Microsoft.AspNetCore.Builder.StatusCodePagesExtensions.UseStatusCodePagesWithRedirects%2A extension method:

  • Sends a 302 — Found status code to the client.
  • Redirects the client to the error handling endpoint provided in the URL template. The error handling endpoint typically displays error information and returns HTTP 200.

:::code language=»csharp» source=»error-handling/samples/6.x/ErrorHandlingSample/Snippets/Program.cs» id=»snippet_UseStatusCodePagesRedirect» highlight=»9″:::

The URL template can include a {0} placeholder for the status code, as shown in the preceding code. If the URL template starts with ~ (tilde), the ~ is replaced by the app’s PathBase. When specifying an endpoint in the app, create an MVC view or Razor page for the endpoint.

This method is commonly used when the app:

  • Should redirect the client to a different endpoint, usually in cases where a different app processes the error. For web apps, the client’s browser address bar reflects the redirected endpoint.
  • Shouldn’t preserve and return the original status code with the initial redirect response.

UseStatusCodePagesWithReExecute

The xref:Microsoft.AspNetCore.Builder.StatusCodePagesExtensions.UseStatusCodePagesWithReExecute%2A extension method:

  • Returns the original status code to the client.
  • Generates the response body by re-executing the request pipeline using an alternate path.

:::code language=»csharp» source=»error-handling/samples/6.x/ErrorHandlingSample/Snippets/Program.cs» id=»snippet_UseStatusCodePagesReExecute» highlight=»9″:::

If an endpoint within the app is specified, create an MVC view or Razor page for the endpoint.

This method is commonly used when the app should:

  • Process the request without redirecting to a different endpoint. For web apps, the client’s browser address bar reflects the originally requested endpoint.
  • Preserve and return the original status code with the response.

The URL template must start with / and may include a placeholder {0} for the status code. To pass the status code as a query-string parameter, pass a second argument into UseStatusCodePagesWithReExecute. For example:

:::code language=»csharp» source=»error-handling/samples/6.x/ErrorHandlingSample/Snippets/Program.cs» id=»snippet_UseStatusCodePagesReExecuteQueryString»:::

The endpoint that processes the error can get the original URL that generated the error, as shown in the following example:

:::code language=»csharp» source=»error-handling/samples/6.x/ErrorHandlingSample/Pages/StatusCode.cshtml.cs» id=»snippet_Class» highlight=»12-21″:::

Disable status code pages

To disable status code pages for an MVC controller or action method, use the [SkipStatusCodePages] attribute.

To disable status code pages for specific requests in a Razor Pages handler method or in an MVC controller, use xref:Microsoft.AspNetCore.Diagnostics.IStatusCodePagesFeature:

:::code language=»csharp» source=»error-handling/samples/6.x/ErrorHandlingSample/Snippets/Pages/Index.cshtml.cs» id=»snippet_OnGet»:::

Exception-handling code

Code in exception handling pages can also throw exceptions. Production error pages should be tested thoroughly and take extra care to avoid throwing exceptions of their own.

Response headers

Once the headers for a response are sent:

  • The app can’t change the response’s status code.
  • Any exception pages or handlers can’t run. The response must be completed or the connection aborted.

Server exception handling

In addition to the exception handling logic in an app, the HTTP server implementation can handle some exceptions. If the server catches an exception before response headers are sent, the server sends a 500 - Internal Server Error response without a response body. If the server catches an exception after response headers are sent, the server closes the connection. Requests that aren’t handled by the app are handled by the server. Any exception that occurs when the server is handling the request is handled by the server’s exception handling. The app’s custom error pages, exception handling middleware, and filters don’t affect this behavior.

Startup exception handling

Only the hosting layer can handle exceptions that take place during app startup. The host can be configured to capture startup errors and capture detailed errors.

The hosting layer can show an error page for a captured startup error only if the error occurs after host address/port binding. If binding fails:

  • The hosting layer logs a critical exception.
  • The dotnet process crashes.
  • No error page is displayed when the HTTP server is Kestrel.

When running on IIS (or Azure App Service) or IIS Express, a 502.5 — Process Failure is returned by the ASP.NET Core Module if the process can’t start. For more information, see xref:test/troubleshoot-azure-iis.

Database error page

The Database developer page exception filter xref:Microsoft.Extensions.DependencyInjection.DatabaseDeveloperPageExceptionFilterServiceExtensions.AddDatabaseDeveloperPageExceptionFilter%2A captures database-related exceptions that can be resolved by using Entity Framework Core migrations. When these exceptions occur, an HTML response is generated with details of possible actions to resolve the issue. This page is enabled only in the Development environment. The following code adds the Database developer page exception filter:

:::code language=»csharp» source=»error-handling/samples/6.x/ErrorHandlingSample/Program.cs» id=»snippet_AddDatabaseDeveloperPageExceptionFilter» highlight=»3″:::

Exception filters

In MVC apps, exception filters can be configured globally or on a per-controller or per-action basis. In Razor Pages apps, they can be configured globally or per page model. These filters handle any unhandled exceptions that occur during the execution of a controller action or another filter. For more information, see xref:mvc/controllers/filters#exception-filters.

Exception filters are useful for trapping exceptions that occur within MVC actions, but they’re not as flexible as the built-in exception handling middleware, xref:Microsoft.AspNetCore.Builder.ExceptionHandlerExtensions.UseExceptionHandler%2A. We recommend using UseExceptionHandler, unless you need to perform error handling differently based on which MVC action is chosen.

Model state errors

For information about how to handle model state errors, see Model binding and Model validation.

Additional resources

  • View or download sample code (how to download)
  • xref:test/troubleshoot-azure-iis
  • xref:host-and-deploy/azure-iis-errors-reference

:::moniker-end

:::moniker range=»>= aspnetcore-5.0 < aspnetcore-6.0″

By Kirk Larkin, Tom Dykstra, and Steve Smith

This article covers common approaches to handling errors in ASP.NET Core web apps. See xref:web-api/handle-errors for web APIs.

View or download sample code. (How to download.) The network tab on the F12 browser developer tools is useful when testing the sample app.

Developer Exception Page

The Developer Exception Page displays detailed information about unhandled request exceptions. The ASP.NET Core templates generate the following code:

:::code language=»csharp» source=»error-handling/samples/5.x/ErrorHandlingSample/Startup.cs» id=»snippet» highlight=»3-6″:::

The preceding highlighted code enables the developer exception page when the app is running in the Development environment.

The templates place xref:Microsoft.AspNetCore.Builder.DeveloperExceptionPageExtensions.UseDeveloperExceptionPage%2A early in the middleware pipeline so that it can catch unhandled exceptions thrown in middleware that follows.

The preceding code enables the Developer Exception Page only when the app runs in the Development environment. Detailed exception information shouldn’t be displayed publicly when the app runs in the Production environment. For more information on configuring environments, see xref:fundamentals/environments.

The Developer Exception Page can include the following information about the exception and the request:

  • Stack trace
  • Query string parameters if any
  • Cookies if any
  • Headers

The Developer Exception Page isn’t guaranteed to provide any information. Use Logging for complete error information.

Exception handler page

To configure a custom error handling page for the Production environment, call xref:Microsoft.AspNetCore.Builder.ExceptionHandlerExtensions.UseExceptionHandler%2A. This exception handling middleware:

  • Catches and logs unhandled exceptions.
  • Re-executes the request in an alternate pipeline using the path indicated. The request isn’t re-executed if the response has started. The template-generated code re-executes the request using the /Error path.

[!WARNING]
If the alternate pipeline throws an exception of its own, Exception Handling Middleware rethrows the original exception.

In the following example, xref:Microsoft.AspNetCore.Builder.ExceptionHandlerExtensions.UseExceptionHandler%2A adds the exception handling middleware in non-Development environments:

:::code language=»csharp» source=»error-handling/samples/5.x/ErrorHandlingSample/Startup.cs» id=»snippet_DevPageAndHandlerPage» highlight=»5-9″:::

The Razor Pages app template provides an Error page (.cshtml) and xref:Microsoft.AspNetCore.Mvc.RazorPages.PageModel class (ErrorModel) in the Pages folder. For an MVC app, the project template includes an Error action method and an Error view for the Home controller.

The exception handling middleware re-executes the request using the original HTTP method. If an error handler endpoint is restricted to a specific set of HTTP methods, it runs only for those HTTP methods. For example, an MVC controller action that uses the [HttpGet] attribute runs only for GET requests. To ensure that all requests reach the custom error handling page, don’t restrict them to a specific set of HTTP methods.

To handle exceptions differently based on the original HTTP method:

  • For Razor Pages, create multiple handler methods. For example, use OnGet to handle GET exceptions and use OnPost to handle POST exceptions.
  • For MVC, apply HTTP verb attributes to multiple actions. For example, use [HttpGet] to handle GET exceptions and use [HttpPost] to handle POST exceptions.

To allow unauthenticated users to view the custom error handling page, ensure that it supports anonymous access.

Access the exception

Use xref:Microsoft.AspNetCore.Diagnostics.IExceptionHandlerPathFeature to access the exception and the original request path in an error handler. The following code adds ExceptionMessage to the default Pages/Error.cshtml.cs generated by the ASP.NET Core templates:

:::code language=»csharp» source=»error-handling/samples/5.x/ErrorHandlingSample/Pages/Error.cshtml.cs» id=»snippet»:::

[!WARNING]
Do not serve sensitive error information to clients. Serving errors is a security risk.

To test the exception in the sample app:

  • Set the environment to production.
  • Remove the comments from webBuilder.UseStartup<Startup>(); in Program.cs.
  • Select Trigger an exception on the home page.

Exception handler lambda

An alternative to a custom exception handler page is to provide a lambda to xref:Microsoft.AspNetCore.Builder.ExceptionHandlerExtensions.UseExceptionHandler%2A. Using a lambda allows access to the error before returning the response.

The following code uses a lambda for exception handling:

:::code language=»csharp» source=»error-handling/samples/5.x/ErrorHandlingSample/StartupLambda.cs» id=»snippet»:::

[!WARNING]
Do not serve sensitive error information from xref:Microsoft.AspNetCore.Diagnostics.IExceptionHandlerFeature or xref:Microsoft.AspNetCore.Diagnostics.IExceptionHandlerPathFeature to clients. Serving errors is a security risk.

To test the exception handling lambda in the sample app:

  • Set the environment to production.
  • Remove the comments from webBuilder.UseStartup<StartupLambda>(); in Program.cs.
  • Select Trigger an exception on the home page.

UseStatusCodePages

By default, an ASP.NET Core app doesn’t provide a status code page for HTTP error status codes, such as 404 — Not Found. When the app sets an HTTP 400-599 error status code that doesn’t have a body, it returns the status code and an empty response body. To provide status code pages, use the status code pages middleware. To enable default text-only handlers for common error status codes, call xref:Microsoft.AspNetCore.Builder.StatusCodePagesExtensions.UseStatusCodePages%2A in the Startup.Configure method:

:::code language=»csharp» source=»error-handling/samples/5.x/ErrorHandlingSample/StartupUseStatusCodePages.cs» id=»snippet» highlight=»13″:::

Call UseStatusCodePages before request handling middleware. For example, call UseStatusCodePages before the Static File Middleware and the Endpoints Middleware.

When UseStatusCodePages isn’t used, navigating to a URL without an endpoint returns a browser-dependent error message indicating the endpoint can’t be found. For example, navigating to Home/Privacy2. When UseStatusCodePages is called, the browser returns:

Status Code: 404; Not Found

UseStatusCodePages isn’t typically used in production because it returns a message that isn’t useful to users.

To test UseStatusCodePages in the sample app:

  • Set the environment to production.
  • Remove the comments from webBuilder.UseStartup<StartupUseStatusCodePages>(); in Program.cs.
  • Select the links on the home page on the home page.

[!NOTE]
The status code pages middleware does not catch exceptions. To provide a custom error handling page, use the exception handler page.

UseStatusCodePages with format string

To customize the response content type and text, use the overload of xref:Microsoft.AspNetCore.Builder.StatusCodePagesExtensions.UseStatusCodePages%2A that takes a content type and format string:

:::code language=»csharp» source=»error-handling/samples/5.x/ErrorHandlingSample/StartupFormat.cs» id=»snippet» highlight=»13-14″:::

In the preceding code, {0} is a placeholder for the error code.

UseStatusCodePages with a format string isn’t typically used in production because it returns a message that isn’t useful to users.

To test UseStatusCodePages in the sample app, remove the comments from webBuilder.UseStartup<StartupFormat>(); in Program.cs.

UseStatusCodePages with lambda

To specify custom error-handling and response-writing code, use the overload of xref:Microsoft.AspNetCore.Builder.StatusCodePagesExtensions.UseStatusCodePages%2A that takes a lambda expression:

:::code language=»csharp» source=»error-handling/samples/5.x/ErrorHandlingSample/StartupStatusLambda.cs» id=»snippet» highlight=»13-20″:::

UseStatusCodePages with a lambda isn’t typically used in production because it returns a message that isn’t useful to users.

To test UseStatusCodePages in the sample app, remove the comments from webBuilder.UseStartup<StartupStatusLambda>(); in Program.cs.

UseStatusCodePagesWithRedirects

The xref:Microsoft.AspNetCore.Builder.StatusCodePagesExtensions.UseStatusCodePagesWithRedirects%2A extension method:

  • Sends a 302 — Found status code to the client.
  • Redirects the client to the error handling endpoint provided in the URL template. The error handling endpoint typically displays error information and returns HTTP 200.

:::code language=»csharp» source=»error-handling/samples/5.x/ErrorHandlingSample/StartupSCredirect.cs» id=»snippet» highlight=»13″:::

The URL template can include a {0} placeholder for the status code, as shown in the preceding code. If the URL template starts with ~ (tilde), the ~ is replaced by the app’s PathBase. When specifying an endpoint in the app, create an MVC view or Razor page for the endpoint. For a Razor Pages example, see Pages/MyStatusCode.cshtml in the sample app.

This method is commonly used when the app:

  • Should redirect the client to a different endpoint, usually in cases where a different app processes the error. For web apps, the client’s browser address bar reflects the redirected endpoint.
  • Shouldn’t preserve and return the original status code with the initial redirect response.

To test UseStatusCodePages in the sample app, remove the comments from webBuilder.UseStartup<StartupSCredirect>(); in Program.cs.

UseStatusCodePagesWithReExecute

The xref:Microsoft.AspNetCore.Builder.StatusCodePagesExtensions.UseStatusCodePagesWithReExecute%2A extension method:

  • Returns the original status code to the client.
  • Generates the response body by re-executing the request pipeline using an alternate path.

:::code language=»csharp» source=»error-handling/samples/5.x/ErrorHandlingSample/StartupSCreX.cs» id=»snippet» highlight=»13″:::

If an endpoint within the app is specified, create an MVC view or Razor page for the endpoint. Ensure UseStatusCodePagesWithReExecute is placed before UseRouting so the request can be rerouted to the status page. For a Razor Pages example, see Pages/MyStatusCode2.cshtml in the sample app.

This method is commonly used when the app should:

  • Process the request without redirecting to a different endpoint. For web apps, the client’s browser address bar reflects the originally requested endpoint.
  • Preserve and return the original status code with the response.

The URL and query string templates may include a placeholder {0} for the status code. The URL template must start with /.

The endpoint that processes the error can get the original URL that generated the error, as shown in the following example:

:::code language=»csharp» source=»error-handling/samples/5.x/ErrorHandlingSample/Pages/MyStatusCode2.cshtml.cs» id=»snippet»:::

For a Razor Pages example, see Pages/MyStatusCode2.cshtml in the sample app.

To test UseStatusCodePages in the sample app, remove the comments from webBuilder.UseStartup<StartupSCreX>(); in Program.cs.

Disable status code pages

To disable status code pages for an MVC controller or action method, use the [SkipStatusCodePages] attribute.

To disable status code pages for specific requests in a Razor Pages handler method or in an MVC controller, use xref:Microsoft.AspNetCore.Diagnostics.IStatusCodePagesFeature:

:::code language=»csharp» source=»error-handling/samples/5.x/ErrorHandlingSample/Pages/Privacy.cshtml.cs» id=»snippet»:::

Exception-handling code

Code in exception handling pages can also throw exceptions. Production error pages should be tested thoroughly and take extra care to avoid throwing exceptions of their own.

Response headers

Once the headers for a response are sent:

  • The app can’t change the response’s status code.
  • Any exception pages or handlers can’t run. The response must be completed or the connection aborted.

Server exception handling

In addition to the exception handling logic in an app, the HTTP server implementation can handle some exceptions. If the server catches an exception before response headers are sent, the server sends a 500 - Internal Server Error response without a response body. If the server catches an exception after response headers are sent, the server closes the connection. Requests that aren’t handled by the app are handled by the server. Any exception that occurs when the server is handling the request is handled by the server’s exception handling. The app’s custom error pages, exception handling middleware, and filters don’t affect this behavior.

Startup exception handling

Only the hosting layer can handle exceptions that take place during app startup. The host can be configured to capture startup errors and capture detailed errors.

The hosting layer can show an error page for a captured startup error only if the error occurs after host address/port binding. If binding fails:

  • The hosting layer logs a critical exception.
  • The dotnet process crashes.
  • No error page is displayed when the HTTP server is Kestrel.

When running on IIS (or Azure App Service) or IIS Express, a 502.5 — Process Failure is returned by the ASP.NET Core Module if the process can’t start. For more information, see xref:test/troubleshoot-azure-iis.

Database error page

The Database developer page exception filter AddDatabaseDeveloperPageExceptionFilter captures database-related exceptions that can be resolved by using Entity Framework Core migrations. When these exceptions occur, an HTML response is generated with details of possible actions to resolve the issue. This page is enabled only in the Development environment. The following code was generated by the ASP.NET Core Razor Pages templates when individual user accounts were specified:

:::code language=»csharp» source=»error-handling/samples/5.x/StartupDBexFilter.cs» id=»snippet» highlight=»6″:::

Exception filters

In MVC apps, exception filters can be configured globally or on a per-controller or per-action basis. In Razor Pages apps, they can be configured globally or per page model. These filters handle any unhandled exceptions that occur during the execution of a controller action or another filter. For more information, see xref:mvc/controllers/filters#exception-filters.

Exception filters are useful for trapping exceptions that occur within MVC actions, but they’re not as flexible as the built-in exception handling middleware, UseExceptionHandler. We recommend using UseExceptionHandler, unless you need to perform error handling differently based on which MVC action is chosen.

:::code language=»csharp» source=»error-handling/samples/5.x/ErrorHandlingSample/Startup.cs» id=»snippet» highlight=»9″:::

Model state errors

For information about how to handle model state errors, see Model binding and Model validation.

Additional resources

  • xref:test/troubleshoot-azure-iis
  • xref:host-and-deploy/azure-iis-errors-reference

:::moniker-end

:::moniker range=»< aspnetcore-5.0″

By Tom Dykstra, and Steve Smith

This article covers common approaches to handling errors in ASP.NET Core web apps. See xref:web-api/handle-errors for web APIs.

View or download sample code. (How to download.)

Developer Exception Page

The Developer Exception Page displays detailed information about request exceptions. The ASP.NET Core templates generate the following code:

:::code language=»csharp» source=»error-handling/samples/2.x/ErrorHandlingSample/Startup.cs» id=»snippet_DevPageAndHandlerPage» highlight=»1-4″:::

The preceding code enables the developer exception page when the app is running in the Development environment.

The templates place xref:Microsoft.AspNetCore.Builder.DeveloperExceptionPageExtensions.UseDeveloperExceptionPage%2A before any middleware so exceptions are caught in the middleware that follows.

The preceding code enables the Developer Exception Page only when the app is running in the Development environment. Detailed exception information should not be displayed publicly when the app runs in production. For more information on configuring environments, see xref:fundamentals/environments.

The Developer Exception Page includes the following information about the exception and the request:

  • Stack trace
  • Query string parameters if any
  • Cookies if any
  • Headers

Exception handler page

To configure a custom error handling page for the Production environment, use the Exception Handling Middleware. The middleware:

  • Catches and logs exceptions.
  • Re-executes the request in an alternate pipeline for the page or controller indicated. The request isn’t re-executed if the response has started. The template generated code re-executes the request to /Error.

In the following example, xref:Microsoft.AspNetCore.Builder.ExceptionHandlerExtensions.UseExceptionHandler%2A adds the Exception Handling Middleware in non-Development environments:

:::code language=»csharp» source=»error-handling/samples/2.x/ErrorHandlingSample/Startup.cs» id=»snippet_DevPageAndHandlerPage» highlight=»5-9″:::

The Razor Pages app template provides an Error page (.cshtml) and xref:Microsoft.AspNetCore.Mvc.RazorPages.PageModel class (ErrorModel) in the Pages folder. For an MVC app, the project template includes an Error action method and an Error view in the Home controller.

Don’t mark the error handler action method with HTTP method attributes, such as HttpGet. Explicit verbs prevent some requests from reaching the method. Allow anonymous access to the method if unauthenticated users should see the error view.

Access the exception

Use xref:Microsoft.AspNetCore.Diagnostics.IExceptionHandlerPathFeature to access the exception and the original request path in an error handler controller or page:

:::code language=»csharp» source=»error-handling/samples/2.x/ErrorHandlingSample/Pages/MyFolder/Error.cshtml.cs» id=»snippet_ExceptionHandlerPathFeature»:::

[!WARNING]
Do not serve sensitive error information to clients. Serving errors is a security risk.

To trigger the preceding exception handling page, set the environment to productions and force an exception.

Exception handler lambda

An alternative to a custom exception handler page is to provide a lambda to xref:Microsoft.AspNetCore.Builder.ExceptionHandlerExtensions.UseExceptionHandler%2A. Using a lambda allows access to the error before returning the response.

Here’s an example of using a lambda for exception handling:

:::code language=»csharp» source=»error-handling/samples/2.x/ErrorHandlingSample/Startup.cs» id=»snippet_HandlerPageLambda»:::

In the preceding code, await context.Response.WriteAsync(new string(' ', 512)); is added so the Internet Explorer browser displays the error message rather than an IE error message. For more information, see this GitHub issue.

[!WARNING]
Do not serve sensitive error information from xref:Microsoft.AspNetCore.Diagnostics.IExceptionHandlerFeature or xref:Microsoft.AspNetCore.Diagnostics.IExceptionHandlerPathFeature to clients. Serving errors is a security risk.

To see the result of the exception handling lambda in the sample app, use the ProdEnvironment and ErrorHandlerLambda preprocessor directives, and select Trigger an exception on the home page.

UseStatusCodePages

By default, an ASP.NET Core app doesn’t provide a status code page for HTTP status codes, such as 404 — Not Found. The app returns a status code and an empty response body. To provide status code pages, use Status Code Pages middleware.

The middleware is made available by the Microsoft.AspNetCore.Diagnostics package.

To enable default text-only handlers for common error status codes, call xref:Microsoft.AspNetCore.Builder.StatusCodePagesExtensions.UseStatusCodePages%2A in the Startup.Configure method:

:::code language=»csharp» source=»error-handling/samples/2.x/ErrorHandlingSample/Startup.cs» id=»snippet_StatusCodePages»:::

Call UseStatusCodePages before request handling middleware (for example, Static File Middleware and MVC Middleware).

When UseStatusCodePages isn’t used, navigating to a URL without an endpoint returns a browser dependent error message indicating the endpoint can’t be found. For example, navigating to Home/Privacy2. When UseStatusCodePages is called, the browser returns:

Status Code: 404; Not Found

UseStatusCodePages with format string

To customize the response content type and text, use the overload of xref:Microsoft.AspNetCore.Builder.StatusCodePagesExtensions.UseStatusCodePages%2A that takes a content type and format string:

:::code language=»csharp» source=»error-handling/samples/2.x/ErrorHandlingSample/Startup.cs» id=»snippet_StatusCodePagesFormatString»:::

UseStatusCodePages with lambda

To specify custom error-handling and response-writing code, use the overload of xref:Microsoft.AspNetCore.Builder.StatusCodePagesExtensions.UseStatusCodePages%2A that takes a lambda expression:

:::code language=»csharp» source=»error-handling/samples/2.x/ErrorHandlingSample/Startup.cs» id=»snippet_StatusCodePagesLambda»:::

UseStatusCodePagesWithRedirects

The xref:Microsoft.AspNetCore.Builder.StatusCodePagesExtensions.UseStatusCodePagesWithRedirects%2A extension method:

  • Sends a 302 — Found status code to the client.
  • Redirects the client to the location provided in the URL template.

:::code language=»csharp» source=»error-handling/samples/2.x/ErrorHandlingSample/Startup.cs» id=»snippet_StatusCodePagesWithRedirect»:::

The URL template can include a {0} placeholder for the status code, as shown in the example. If the URL template starts with ~ (tilde), the ~ is replaced by the app’s PathBase. If you point to an endpoint within the app, create an MVC view or Razor page for the endpoint. For a Razor Pages example, see Pages/StatusCode.cshtml in the sample app.

This method is commonly used when the app:

  • Should redirect the client to a different endpoint, usually in cases where a different app processes the error. For web apps, the client’s browser address bar reflects the redirected endpoint.
  • Shouldn’t preserve and return the original status code with the initial redirect response.

UseStatusCodePagesWithReExecute

The xref:Microsoft.AspNetCore.Builder.StatusCodePagesExtensions.UseStatusCodePagesWithReExecute%2A extension method:

  • Returns the original status code to the client.
  • Generates the response body by re-executing the request pipeline using an alternate path.

:::code language=»csharp» source=»error-handling/samples/2.x/ErrorHandlingSample/Startup.cs» id=»snippet_StatusCodePagesWithReExecute»:::

If you point to an endpoint within the app, create an MVC view or Razor page for the endpoint. Ensure UseStatusCodePagesWithReExecute is placed before UseRouting so the request can be rerouted to the status page. For a Razor Pages example, see Pages/StatusCode.cshtml in the sample app.

This method is commonly used when the app should:

  • Process the request without redirecting to a different endpoint. For web apps, the client’s browser address bar reflects the originally requested endpoint.
  • Preserve and return the original status code with the response.

The URL and query string templates may include a placeholder ({0}) for the status code. The URL template must start with a slash (/). When using a placeholder in the path, confirm that the endpoint (page or controller) can process the path segment. For example, a Razor Page for errors should accept the optional path segment value with the @page directive:

The endpoint that processes the error can get the original URL that generated the error, as shown in the following example:

:::code language=»csharp» source=»error-handling/samples/2.x/ErrorHandlingSample/Pages/StatusCode.cshtml.cs» id=»snippet_StatusCodeReExecute»:::

Don’t mark the error handler action method with HTTP method attributes, such as HttpGet. Explicit verbs prevent some requests from reaching the method. Allow anonymous access to the method if unauthenticated users should see the error view.

Disable status code pages

To disable status code pages for an MVC controller or action method, use the [SkipStatusCodePages] attribute.

To disable status code pages for specific requests in a Razor Pages handler method or in an MVC controller, use xref:Microsoft.AspNetCore.Diagnostics.IStatusCodePagesFeature:

var statusCodePagesFeature = HttpContext.Features.Get<IStatusCodePagesFeature>();

if (statusCodePagesFeature != null)
{
    statusCodePagesFeature.Enabled = false;
}

Exception-handling code

Code in exception handling pages can throw exceptions. It’s often a good idea for production error pages to consist of purely static content.

Response headers

Once the headers for a response are sent:

  • The app can’t change the response’s status code.
  • Any exception pages or handlers can’t run. The response must be completed or the connection aborted.

Server exception handling

In addition to the exception handling logic in your app, the HTTP server implementation can handle some exceptions. If the server catches an exception before response headers are sent, the server sends a 500 — Internal Server Error response without a response body. If the server catches an exception after response headers are sent, the server closes the connection. Requests that aren’t handled by your app are handled by the server. Any exception that occurs when the server is handling the request is handled by the server’s exception handling. The app’s custom error pages, exception handling middleware, and filters don’t affect this behavior.

Startup exception handling

Only the hosting layer can handle exceptions that take place during app startup. The host can be configured to capture startup errors and capture detailed errors.

The hosting layer can show an error page for a captured startup error only if the error occurs after host address/port binding. If binding fails:

  • The hosting layer logs a critical exception.
  • The dotnet process crashes.
  • No error page is displayed when the HTTP server is Kestrel.

When running on IIS (or Azure App Service) or IIS Express, a 502.5 — Process Failure is returned by the ASP.NET Core Module if the process can’t start. For more information, see xref:test/troubleshoot-azure-iis.

Database error page

Database Error Page Middleware captures database-related exceptions that can be resolved by using Entity Framework migrations. When these exceptions occur, an HTML response with details of possible actions to resolve the issue is generated. This page should be enabled only in the Development environment. Enable the page by adding code to Startup.Configure:

if (env.IsDevelopment())
{
    app.UseDatabaseErrorPage();
}

xref:Microsoft.AspNetCore.Builder.DatabaseErrorPageExtensions.UseDatabaseErrorPage%2A requires the Microsoft.AspNetCore.Diagnostics.EntityFrameworkCore NuGet package.

Exception filters

In MVC apps, exception filters can be configured globally or on a per-controller or per-action basis. In Razor Pages apps, they can be configured globally or per page model. These filters handle any unhandled exception that occurs during the execution of a controller action or another filter. For more information, see xref:mvc/controllers/filters#exception-filters.

[!TIP]
Exception filters are useful for trapping exceptions that occur within MVC actions, but they’re not as flexible as the Exception Handling Middleware. We recommend using the middleware. Use filters only where you need to perform error handling differently based on which MVC action is chosen.

Model state errors

For information about how to handle model state errors, see Model binding and Model validation.

Additional resources

  • xref:test/troubleshoot-azure-iis
  • xref:host-and-deploy/azure-iis-errors-reference

:::moniker-end

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.

Quick and Easy Exception Handling

Simply add this middleware before ASP.NET routing into your middleware registrations.

app.UseExceptionHandler(c => c.Run(async context =>
{
    var exception = context.Features
        .Get<IExceptionHandlerPathFeature>()
        .Error;
    var response = new { error = exception.Message };
    await context.Response.WriteAsJsonAsync(response);
}));
app.UseMvc(); // or .UseRouting() or .UseEndpoints()

Done!


Enable Dependency Injection for logging and other purposes

Step 1. In your startup, register your exception handling route:

// It should be one of your very first registrations
app.UseExceptionHandler("/error"); // Add this
app.UseEndpoints(endpoints => endpoints.MapControllers());

Step 2. Create controller that will handle all exceptions and produce error response:

[AllowAnonymous]
[ApiExplorerSettings(IgnoreApi = true)]
public class ErrorsController : ControllerBase
{
    [Route("error")]
    public MyErrorResponse Error()
    {
        var context = HttpContext.Features.Get<IExceptionHandlerFeature>();
        var exception = context.Error; // Your exception
        var code = 500; // Internal Server Error by default

        if      (exception is MyNotFoundException) code = 404; // Not Found
        else if (exception is MyUnauthException)   code = 401; // Unauthorized
        else if (exception is MyException)         code = 400; // Bad Request

        Response.StatusCode = code; // You can use HttpStatusCode enum instead

        return new MyErrorResponse(exception); // Your error model
    }
}

A few important notes and observations:

  • You can inject your dependencies into the Controller’s constructor.
  • [ApiExplorerSettings(IgnoreApi = true)] is needed. Otherwise, it may break your Swashbuckle swagger
  • Again, app.UseExceptionHandler("/error"); has to be one of the very top registrations in your Startup Configure(...) method. It’s probably safe to place it at the top of the method.
  • The path in app.UseExceptionHandler("/error") and in controller [Route("error")] should be the same, to allow the controller handle exceptions redirected from exception handler middleware.

Here is the link to official Microsoft documentation.


Response model ideas.

Implement your own response model and exceptions.
This example is just a good starting point. Every service would need to handle exceptions in its own way. With the described approach you have full flexibility and control over handling exceptions and returning the right response from your service.

An example of error response model (just to give you some ideas):

public class MyErrorResponse
{
    public string Type { get; set; }
    public string Message { get; set; }
    public string StackTrace { get; set; }

    public MyErrorResponse(Exception ex)
    {
        Type = ex.GetType().Name;
        Message = ex.Message;
        StackTrace = ex.ToString();
    }
}

For simpler services, you might want to implement http status code exception that would look like this:

public class HttpStatusException : Exception
{
    public HttpStatusCode Status { get; private set; }

    public HttpStatusException(HttpStatusCode status, string msg) : base(msg)
    {
        Status = status;
    }
}

This can be thrown from anywhere this way:

throw new HttpStatusCodeException(HttpStatusCode.NotFound, "User not found");

Then your handling code could be simplified to just this:

if (exception is HttpStatusException httpException)
{
    code = (int) httpException.Status;
}

HttpContext.Features.Get<IExceptionHandlerFeature>() WAT?

ASP.NET Core developers embraced the concept of middlewares where different aspects of functionality such as Auth, MVC, Swagger etc. are separated and executed sequentially in the request processing pipeline. Each middleware has access to request context and can write into the response if needed. Taking exception handling out of MVC makes sense if it’s important to handle errors from non-MVC middlewares the same way as MVC exceptions, which I find is very common in real world apps. So because built-in exception handling middleware is not a part of MVC, MVC itself knows nothing about it and vice versa, exception handling middleware doesn’t really know where the exception is coming from, besides of course it knows that it happened somewhere down the pipe of request execution. But both may needed to be «connected» with one another. So when exception is not caught anywhere, exception handling middleware catches it and re-runs the pipeline for a route, registered in it. This is how you can «pass» exception handling back to MVC with consistent content negotiation or some other middleware if you wish. The exception itself is extracted from the common middleware context. Looks funny but gets the job done :).

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! 😀

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! <<

This is the eighth of a new series of posts on ASP .NET Core 3.1 for 2020. In this series, we’ll cover 26 topics over a span of 26 weeks from January through June 2020, titled ASP .NET Core A-Z! To differentiate from the 2019 series, the 2020 series will mostly focus on a growing single codebase (NetLearner!) instead of new unrelated code snippets week.

Previous post:

  • Generic Host Builder in ASP .NET Core 3.1

NetLearner on GitHub :

  • Repository: https://github.com/shahedc/NetLearnerApp
  • v0.8-alpha release: https://github.com/shahedc/NetLearnerApp/releases/tag/v0.8-alpha

In this Article:

  • H is for Handling Errors
  • Exceptions with Try-Catch-Finally
  • Try-Catch-Finally in NetLearner
  • Error Handling for MVC
  • Error Handling for Razor Pages
  • Error Handling in Blazor
  • Logging Errors
  • Transient Fault Handling
  • References

H is for Handling Errors

Unless you’re perfect 100% of the time (who is?), you’ll most likely have errors in your code. If your code doesn’t build due to compilation errors, you can probably correct that by fixing the offending code. But if your application encounters runtime errors while it’s being used, you may not be able to anticipate every possible scenario.

Runtime errors may cause Exceptions, which can be caught and handled in many programming languages. Unit tests will help you write better code, minimize errors and create new features with confidence. In the meantime, there’s the good ol’ try-catch-finally block, which should be familiar to most developers.

NOTE: You may skip to the next section below if you don’t need this refresher.

try-catch-finally in C#

Exceptions with Try-Catch-Finally

The simplest form of a try-catch block looks something like this:

**try** { _ **// try something here** _} **catch** ( **Exception** ex){ _ **// catch an exception here** _}

You can chain multiple catch blocks, starting with more specific exceptions. This allows you to catch more generic exceptions toward the end of your try-catch code. In a string of _ catch _() blocks, only the caught exception (if any) will cause that block of code to run.

**try** { _ **// try something here** _} **catch** ( **IOException** ioex){ _ **// catch specific exception, e.g. IOException** _} **catch (Exception** ex){ _ **// catch generic exception here** _}

Finally, you can add the optional _ finally _block. Whether or not an exception has occurred, the finally block will always be executed.

**try** { _ **// try something here** _} **catch** ( **IOException** ioex){ _ **// catch specific exception, e.g. IOException** _} **catch (Exception** ex){ _ **// catch generic exception here** _} **finally** { _ **// always run this code** _}

Try-Catch-Finally in NetLearner

In the NetLearner sample app, the LearningResourcesController uses a LearningResourceService from a shared .NET Standard Library to handle database updates. In the overloaded Edit() method, it wraps a call to the Update() method from the service class to catch a possible DbUpdateConcurrencyException exception. First, it checks to see whether the ModelState is valid or not.

if (ModelState.IsValid){ **...** }

Then, it tries to update user-submitted data by passing a LearningResource object. If a DbUpdateConcurrencyException exception occurs, there is a check to verify whether the current LearningResource even exists or not, so that a 404 (NotFound) can be returned if necessary.

**try** { await _learningResourceService.Update(learningResource);} **catch** ( **DbUpdateConcurrencyException** ){ if (!LearningResourceExists(learningResource.Id)) { return NotFound(); } else { **throw** ; }}return RedirectToAction(nameof(Index));

In the above code, you can also see a throw keyword by itself, when the expected exception occurs if the Id is found to exist. In this case, the throw statement (followed immediately by a semicolon) ensures that the exception is rethrown, while preserving the stack trace.

Run the MVC app and navigate to the Learning Resources page after adding some items. If there are no errors, you should see just the items retrieved from the database.

Learning Resources UI (no errors)

If an exception occurs in the Update() method, this will cause the expected exception to be thrown. To simulate this exception, you can intentionally throw the exception inside the update method. This should cause the error to be displayed in the web UI when attempting to save a learning resource.

Exception details in Web UI

Error Handling for MVC

In ASP .NET Core MVC web apps, unhandled exceptions are typically handled in different ways, depending on whatever environment the app is running in. The default template uses the DeveloperExceptionPage middleware in a development environment but redirects to a shared Error view in non-development scenarios. This logic is implemented in the Configure () method of the Startup.cs class.

if (env. **IsDevelopment** ()){ app. **UseDeveloperExceptionPage** (); app. **UseDatabaseErrorPage** (); }else{ **app.UseExceptionHandler("/Home/Error");** _ **...** _}

The DeveloperExceptionPage middleware can be further customized with DeveloperExceptionPageOptions properties, such as FileProvider and SourceCodeLineCount.

var options = new **DeveloperExceptionPageOptions** { **SourceCodeLineCount** = 2}; app.UseDeveloperExceptionPage(options); 

Using the snippet shown above, the error page will show the offending line in red, with a variable number of lines of code above it. The number of lines is determined by the value of SourceCodeLineCount , which is set to 2 in this case. In this contrived example, I’m forcing the exception by throwing a new Exception in my code.

Customized lines of code within error page

For non-dev scenarios, the shared Error view can be further customized by updating the Error.cshtml view in the Shared subfolder. The ErrorViewModel has a ShowRequestId boolean value that can be set to true to see the RequestId value.

@model **ErrorViewModel** @{ ViewData["Title"] = "Error";}<h1 class="text-danger">Error.</h1><h2 class="text-danger">An error occurred while processing your request.</h2>@if (Model. **ShowRequestId** ){<p><strong>Request ID:</strong> <code>@Model. **RequestId** </code></p>}<h3>header content</h3><p>text content</p> 

In the MVC project’s Home Controller, the Error () action method sets the RequestId to the current Activity.Current.Id if available or else it uses HttpContext.TraceIdentifier. These values can be useful during debugging.

[**ResponseCache** (Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)]public IActionResult **Error** (){ return View(new **ErrorViewModel** { **RequestId** = Activity.Current?.Id ?? HttpContext.TraceIdentifier });} 

But wait… what about Web API in ASP .NET Core? After posting the 2019 versions of this article in a popular ASP .NET Core group on Facebook, I got some valuable feedback from the admin:

Dmitry Pavlov: “For APIs there is a nice option to handle errors globally with the custom middleware https://code-maze.com/global-error-handling-aspnetcore – helps to get rid of try/catch-es in your code. Could be used together with FluentValidation and MediatR – you can configure mapping specific exception types to appropriate status codes (400 bad response, 404 not found, and so on to make it more user friendly and avoid using 500 for everything).”

For more information on the aforementioned items, check out the following resources:

  • Global Error Handling in ASP.NET Core Web API: https://code-maze.com/global-error-handling-aspnetcore/
  • FluentValidation • ASP.NET Integration: https://fluentvalidation.net/aspnet
  • MediatR Wiki: https://github.com/jbogard/MediatR/wiki
  • Using MediatR in ASPNET Core Apps: https://ardalis.com/using-mediatr-in-aspnet-core-apps

Later on in this series, we’ll cover ASP .NET Core Web API in more detail, when we get to “W is for Web API”. Stay tuned!

Error Handling for Razor Pages

Since Razor Pages still use the MVC middleware pipeline, the exception handling is similar to the scenarios described above. For starters, here’s what the Configure () method looks like in the Startup.cs file for the Razor Pages web app sample.

if (env. **IsDevelopment** ()){ app. **UseDeveloperExceptionPage** (); app. **UseDatabaseErrorPage** (); }else{ app. **UseExceptionHandler** ("/Error"); ...}

In the above code, you can see the that development environment uses the same DeveloperExceptionPage middleware. This can be customized using the same techniques outlined in the previous section for MVC pages, so we won’t go over this again.

As for the non-dev scenario, the exception handler is slightly different for Razor Pages. Instead of pointing to the Home controller’s Error () action method (as the MVC version does), it points to the to the / Error page route. This Error.cshtml Razor Page found in the root level of the Pages folder.

@ **page** @model **ErrorModel** @{ ViewData["Title"] = "Error";}<h1 class="text-danger">Error.</h1><h2 class="text-danger">An error occurred while processing your request.</h2>@if (Model. **ShowRequestId** ){ <p> <strong>Request ID:</strong> <code>@Model. **RequestId** </code> </p>}<h3>custom header text</h3><p>custom body text</p>

The above Error page for looks almost identical to the Error view we saw in the previous section, with some notable differences:

  • @ page directive (required for Razor Pages, no equivalent for MVC view)
  • uses ErrorModel (associated with Error page) instead of ErrorViewModel (served by Home controller’s action method)

Error Handling for Blazor

In Blazor web apps, the UI for error handling is included in the Blazor project templates. Consequently, this UI is available in the NetLearner Blazor sample as well. The _Host.cshtml file in the Pages folder holds the following < environment > elements:

<div id="blazor-error-ui"> < **environment** include="Staging,Production"> An error has occurred. This application may no longer respond until reloaded. </ **environment** > < **environment** include="Development"> An unhandled exception has occurred. See browser dev tools for details. </ **environment** > <a href="" class="reload">Reload</a> <a class="dismiss">🗙</a> </div>

The div identified by the id “blazor-error-ui” ensures that is hidden by default, but only shown when an error has occured. Server-side Blazor maintains a connection to the end-user by preserving state with the use of a so-called circuit.

Each browser/tab instance initiates a new circuit. An unhandled exception may terminate a circuit. In this case, the user will have to reload their browser/tab to establish a new circuit.

According to the official documentation, unhandled exceptions may occur in the following areas:

  1. Component instantiation : when constructor invoked
  2. Lifecycle methods : (see Blazor post for details)
  3. Upon rendering : during BuildRenderTree() in .razor component
  4. UI Events : e.g. onclick events, data binding on UI elements
  5. During disposal : while component’s .Dispose() method is called
  6. JavaScript Interop : during calls to IJSRuntime.InvokeAsync
  7. Prerendering : when using the Component tag helper

To avoid unhandled exceptions, use try-catch exception handlers within .razor components to display error messages that are visible to the user. For more details on various scenarios, check out the official documentation at:

  • Handle errors in ASP.NET Core Blazor apps: https://docs.microsoft.com/en-us/aspnet/core/blazor/handle-errors?view=aspnetcore-3.1

Logging Errors

To log errors in ASP .NET Core, you can use the built-in logging features or 3rd-party logging providers. In ASP .NET Core 2.x, the use of CreateDefaultBuilder () in Program.cs takes of care default Logging setup and configuration (behind the scenes).

public static IWebHostBuilder **CreateWebHostBuilder** (string[] args) => WebHost. **CreateDefaultBuilder** (args) .UseStartup<Startup>();

The Web Host Builder was replaced by the Generic Host Builder in ASP .NET Core 3.0, so it looks slightly different now. For more information on Generic Host Builder, take a look at the previous blog post in this series: Generic Host Builder in ASP .NET Core.

public static IHostBuilder **CreateHostBuilder** (string[] args) => Host. **CreateDefaultBuilder** (args) . **ConfigureWebHostDefaults** (webBuilder => { webBuilder. **UseStartup** (); });

The host can be used to set up logging configuration, e.g.:

public static IHostBuilder **CreateHostBuilder** (string[] args) => Host. **CreateDefaultBuilder** (args) . **ConfigureLogging** ((hostingContext, logging) => { logging.ClearProviders(); logging.AddConsole(options => options.IncludeScopes = true); logging.AddDebug(); }) . **ConfigureWebHostDefaults** (webBuilder => { webBuilder. **UseStartup** (); });

To make use of error logging (in addition to other types of logging) in your web app, you may call the necessary methods in your controller’s action methods or equivalent. Here, you can log various levels of information, warnings and errors at various severity levels.

As seen in the snippet below, you have to do the following in your _ MVC Controller _ that you want to add Logging to:

  1. Add using statement for Logging namespace
  2. Add a private readonly variable for an ILogger object
  3. Inject an ILogger object into the constructor
  4. Assign the private variable to the injected variable
  5. Call various log logger methods as needed.
... **using Microsoft.Extensions.Logging;** public class **MyController** : Controller{ ... **private**  **readonly ILogger _logger;** public **MyController** (..., ILogger< **MyController** > logger) { ... **_logger = logger;** } public IActionResult MyAction(...) { _logger. **LogTrace** ("log trace"); _logger. **LogDebug** ("log debug"); _logger. **LogInformation** ("log info"); _logger. **LogWarning** ("log warning"); _logger. **LogError** ("log error"); _logger. **LogCritical** ("log critical"); }}

In Razor Pages, the logging code will go into the Page’s corresponding Model class. As seen in the snippet below, you have to do the following to the _ Model class that corresponds to a Razor Page _:

  1. Add using statement for Logging namespace
  2. Add a private readonly variable for an ILogger object
  3. Inject an ILogger object into the constructor
  4. Assign the private variable to the injected variable
  5. Call various log logger methods as needed.
... **using Microsoft.Extensions.Logging;** public class **MyPageModel** : PageModel{ ... **private readonly ILogger _logger;** public **MyPageModel** (..., **ILogger<MyPageModel> logger** ) { ... **_logger = logger;** } ... public void **MyPageMethod** () { ... _logger. **LogInformation** ("log info"); _logger. **LogError** ("log error"); ... }} 

You may have noticed that Steps 1 through 5 are pretty much identical for MVC and Razor Pages. This makes it very easy to quickly add all sorts of logging into your application, including error logging.

Transient fault handling

Although it’s beyond the scope of this article, it’s worth mentioning that you can avoid transient faults (e.g. temporary database connection losses) by using some proven patterns, practices and existing libraries. To get some history on transient faults, check out the following article from the classic “patterns & practices”. It describes the so-called “Transient Fault Handling Application Block”.

  • Classic Patterns & Practices: https://docs.microsoft.com/en-us/previous-versions/msp-n-p/dn440719(v=pandp.60)

More recently, check out the docs on Transient Fault Handling:

  • Docs: https://docs.microsoft.com/en-us/aspnet/aspnet/overview/developing-apps-with-windows-azure/building-real-world-cloud-apps-with-windows-azure/transient-fault-handling

And now in .NET Core, you can add resilience and transient fault handling to your .NET Core HttpClient with Polly!

  • Adding Resilience and Transient Fault handling to your .NET Core HttpClient with Polly: https://www.hanselman.com/blog/AddingResilienceAndTransientFaultHandlingToYourNETCoreHttpClientWithPolly.aspx
  • Integrating with Polly for transient fault handling: https://www.stevejgordon.co.uk/httpclientfactory-using-polly-for-transient-fault-handling
  • Using Polly for .NET Resilience with .NET Core: https://www.telerik.com/blogs/using-polly-for-net-resilience-and-transient-fault-handling-with-net-core

You can get more information on the Polly project on the official Github page:

  • Polly on Github: https://github.com/App-vNext/Polly

References

  • try-catch-finally: https://docs.microsoft.com/en-us/dotnet/csharp/language-reference/keywords/try-catch-finally
  • Handle errors in ASP.NET Core: https://docs.microsoft.com/en-us/aspnet/core/fundamentals/error-handling
  • Use multiple environments in ASP.NET Core: https://docs.microsoft.com/en-us/aspnet/core/fundamentals/environments
  • UseDeveloperExceptionPage: https://docs.microsoft.com/en-us/dotnet/api/microsoft.aspnetcore.builder.developerexceptionpageextensions.usedeveloperexceptionpage
  • DeveloperExceptionPageOptions: https://docs.microsoft.com/en-us/dotnet/api/microsoft.aspnetcore.builder.developerexceptionpageoptions
  • Logging: https://docs.microsoft.com/en-us/aspnet/core/fundamentals/logging
  • Handle errors in ASP.NET Core Blazor apps: https://docs.microsoft.com/en-us/aspnet/core/blazor/handle-errors?view=aspnetcore-3.1

Handling errors in an ASP.NET Core Web API

This post looks at the best ways to handle exceptions, validation and other invalid requests such as 404s in ASP.NET Core Web API projects and how these approaches differ from MVC error handling.

Why do we need a different approach from MVC?

In .Net Core, MVC and Web API have been combined so you now have the same controllers for both MVC actions and API actions. However, despite the similarities, when it comes to error handling, you almost certainly want to use a different approach for API errors.

MVC actions are typically executed as a result of a user action in the browser so returning an error page to the browser is the correct approach. With an API, this is not generally the case.

API calls are most often called by back-end code or javascript code and in both cases, you never want to simply display the response from the API. Instead we check the status code and parse the response to determine if our action was successful, displaying data to the user as necessary. An error page is not helpful in these situations. It bloats the response with HTML and makes client code difficult because JSON (or XML) is expected, not HTML.

While we want to return information in a different format for Web API actions, the techniques for handling errors are not so different from MVC. Much of the time, it is practically the same flow but instead of returning a View, we return JSON. Let’s look at a few examples.

The minimal approach

With MVC actions, failure to display a friendly error page is unacceptable in a professional application. With an API, while not ideal, empty response bodies are far more permissible for many invalid request types. Simply returning a 404 status code (with no response body) for an API route that does not exist may provide the client with enough information to fix their code.

With zero configuration, this is what ASP.NET Core gives us out of the box.

Depending on your requirements, this may be acceptable for many common status codes but it will rarely be sufficient for validation failures. If a client passes you invalid data, returning a 400 Bad Request is not going to be helpful enough for the client to diagnose the problem. At a minimum, we need to let them know which fields are incorrect and ideally, we would return an informative message for each failure.

With ASP.NET Web API, this is trivial. Assuming that we are using model binding, we get validation for free by using data annotations and/or IValidatableObject. Returning the validation information to the client as JSON is one easy line of code.

Here is our model:

public class GetProductRequest : IValidatableObject
{
    [Required]
    public string ProductId { get; set; }

    public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
    {
        if (...)
        {
            yield return new ValidationResult("ProductId is invalid", new[] { "ProductId" });
        }
    }
}

And our controller action:

[HttpGet("product")]
public IActionResult GetProduct(GetProductRequest request)
{
    if (!ModelState.IsValid)
    {
        return BadRequest(ModelState);
    }

    ...
}

A missing ProductId results in a 400 status code plus a JSON response body similar to the following:

{
    "ProductId":["The ProductId field is required."]
}

This provides an absolute minimum for a client to consume our service but it is not difficult to improve upon this baseline and create a much better client experience. In the next few sections we will look at how simple it is to take our service to the next level.

Returning additional information for specific errors

If we decide that a status code only approach is too bare-bones, it is easy to provide additional information. This is highly recommended. There are many situations where a status code by itself is not enough to determine the cause of failure. If we take a 404 status code as an example, in isolation, this could mean:

  • We are making the request to the wrong site entirely (perhaps the ‘www’ site rather than the ‘api’ subdomain)
  • The domain is correct but the URL does not match a route
  • The URL correctly maps to a route but the resource does not exist

If we could provide information to distinguish between these cases, it could be very useful for a client. Here is our first attempt at dealing with the last of these:

[HttpGet("product")]
public async Task<IActionResult> GetProduct(GetProductRequest request)
{
    ...

    var model = await _db.Get(...);

    if (model == null)
    {
        return NotFound("Product not found");
    }

    return Ok(model);
}

We are now returning a more useful message but it is far from perfect. The main problem is that by using a string in the NotFound method, the framework will return this string as a plain text response rather than JSON.

As a client, a service returning a different content type for certain errors is much harder to deal with than a consistent JSON service.

This issue can quickly be rectified by changing the code to what is shown below but in the next section, we will talk about a better alternative.

return NotFound(new { message = "Product not found" });

Customising the response structure for consistency

Constructing anonymous objects on the fly is not the approach to take if you want a consistent client experience. Ideally our API should return the same response structure in all cases, even when the request was unsuccessful.

Let’s define a base ApiResponse class:

public class ApiResponse
{
    public int StatusCode { get; }

    [JsonProperty(NullValueHandling = NullValueHandling.Ignore)]
    public string Message { get; }

    public ApiResponse(int statusCode, string message = null)
    {
        StatusCode = statusCode;
        Message = message ?? GetDefaultMessageForStatusCode(statusCode);
    }

    private static string GetDefaultMessageForStatusCode(int statusCode)
    {
        switch (statusCode)
        {
            ...
            case 404:
                return "Resource not found";
            case 500:
                return "An unhandled error occurred";
            default:
                return null;
        }
    }
}

We’ll also need a derived ApiOkResponse class that allows us to return data:

public class ApiOkResponse : ApiResponse
{
    public object Result { get; }

    public ApiOkResponse(object result)
        :base(200)
    {
        Result = result;
    }
}

Finally, let’s declare an ApiBadRequestResponse class to handle validation errors (if we want our responses to be consistent, we will need to replace the built-in functionality used above).

public class ApiBadRequestResponse : ApiResponse
{
    public IEnumerable<string> Errors { get; }

    public ApiBadRequestResponse(ModelStateDictionary modelState)
        : base(400)
    {
        if (modelState.IsValid)
        {
            throw new ArgumentException("ModelState must be invalid", nameof(modelState));
        }

        Errors = modelState.SelectMany(x => x.Value.Errors)
            .Select(x => x.ErrorMessage).ToArray();
    }
}

These classes are very simple but can be customised to your own requirements.

If we change our action to use these ApiResponse based classes, it becomes:

[HttpGet("product")]
public async Task<IActionResult> GetProduct(GetProductRequest request)
{
    if (!ModelState.IsValid)
    {
        return BadRequest(new ApiBadRequestResponse(ModelState));
    }

    var model = await _db.Get(...);

    if (model == null)
    {
        return NotFound(new ApiResponse(404, $"Product not found with id {request.ProductId}"));
    }

    return Ok(new ApiOkResponse(model));
}

The code is slightly more complicated now but all three types of response from our action (success, bad request and not found) now use the same general structure.

Centralising Validation Logic

Given that validation is something that you do in practically every action, it makes to refactor this generic code into an action filter. This reduces the size of our actions, removes duplicated code and improves consistency.

public class ApiValidationFilterAttribute : ActionFilterAttribute
{
    public override void OnActionExecuting(ActionExecutingContext context)
    {
        if (!context.ModelState.IsValid)
        {
            context.Result = new BadRequestObjectResult(new ApiBadRequestResponse(context.ModelState));
        }

        base.OnActionExecuting(context);
    }
}

Handling global errors

Responding to bad input in our controller actions is the best way to provide specific error information to our client. Sometimes however, we need to respond to more generic issues. Examples of this include:

  • A 401 Unauthorized code returned from security middleware.

  • A request URL that does not map to a controller action resulting in a 404.

  • Global exceptions. Unless you can do something about a specific exception, you should not clutter your actions with try catch blocks.

As with MVC, the easiest way to deal with global errors is by using StatusCodePagesWithReExecute and UseExceptionHandler.

We talked about StatusCodePagesWithReExecute last time but to reiterate, when a non-success status code is returned from inner middleware (such as an API action), the middleware allows you to execute another action to deal with the status code and return a custom response.

UseExceptionHandler works in a similar way, catching and logging unhandled exceptions and allowing you to execute another action to handle the error. In this example, we configure both pieces of middleware to point to the same action.

We add the middleware in startup.cs:

app.UseStatusCodePagesWithReExecute("/error/{0}");
app.UseExceptionHandler("/error/500");
...
//register other middleware that might return a non-success status code

Then we add our error handling action:

[Route("error/{code}")]
public IActionResult Error(int code)
{
    return new ObjectResult(new ApiResponse(code));
}

With this in place, all exceptions and non-success status codes (without a response body) will be handled by our error action where we return our standard ApiResponse.

Custom Middleware

For the ultimate in control, you can replace or complement built-in middleware with your own custom middleware. The example below handles any bodiless response and returns our simple ApiResponse object as JSON. If this is used in conjunction with code in our actions to return ApiResponse objects, we can ensure that both success and failure responses share the same common structure and all requests result in both a status code and a consistent JSON body:

public class ErrorWrappingMiddleware
{
    private readonly RequestDelegate _next;
    private readonly ILogger<ErrorWrappingMiddleware> _logger;
    
    public ErrorWrappingMiddleware(RequestDelegate next, ILogger<ErrorWrappingMiddleware> logger)
    {
        _next = next;
        _logger = logger ?? throw new ArgumentNullException(nameof(logger));
    }

    public async Task Invoke(HttpContext context)
    {
        try
        {
            await _next.Invoke(context);
        }
        catch(Exception ex)
        {
            _logger.LogError(EventIds.GlobalException, ex, ex.Message);

            context.Response.StatusCode = 500;
        }            

        if (!context.Response.HasStarted)
        {
            context.Response.ContentType = "application/json";

            var response = new ApiResponse(context.Response.StatusCode);

            var json = JsonConvert.SerializeObject(response);

            await context.Response.WriteAsync(json);
        }            
    }
}

Conclusion

Handling errors in ASP.NET Core APIs is similar but different from MVC error code. At the action level, we want to return custom objects (serialised as JSON) rather than custom views.

For generic errors, we can still use the StatusCodePagesWithReExecute middleware but need to modify our code to return an ObjectResult instead of a ViewResult.

For full control, it is not difficult to write your own middleware to handle errors exactly as required.

Useful or Interesting?

If you liked the article, I would really appreciate it if you could share it with your Twitter followers.

Share
on Twitter

Comments

Понравилась статья? Поделить с друзьями:
  • Error handle does not name a type
  • Error h06 webasto
  • Error h is not a valid key name line 007 hotkey как исправить
  • Error guru meditation 78eac4 88 39fdba failed to obtain anisette 500 internal server error
  • Error guf на тонометре