Introduction to Error Handling in ASP.NET Core
By Steve Smith and Tom Dykstra
This article covers common appoaches to handling errors in ASP.NET Core apps.
View or download sample code ((xref:)how to download)
The developer exception page
To configure an app to display a page that shows detailed information about exceptions, install the Microsoft.AspNetCore.Diagnostics
NuGet package and add a line to the Configure method in the Startup class:
[!code-csharpMain]
1: #define StatusCodePages // or StatusCodePagesWithRedirect
2:
3: using System;
4: using System.Collections.Generic;
5: using System.Linq;
6: using System.Threading.Tasks;
7: using Microsoft.AspNetCore.Builder;
8: using Microsoft.AspNetCore.Hosting;
9: using Microsoft.AspNetCore.Http;
10: using Microsoft.Extensions.DependencyInjection;
11: using Microsoft.Extensions.Logging;
12: using System.Text;
13: using System.Text.Encodings.Web;
14: using System.Net;
15: using Microsoft.AspNetCore.Diagnostics;
16:
17: namespace ErrorHandlingSample
18: {
19: public class Startup
20: {
21: public void ConfigureServices(IServiceCollection services)
22: {
23: }
24:
25: #region snippet_DevExceptionPage
26: public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)
27: {
28: loggerFactory.AddConsole();
29: env.EnvironmentName = EnvironmentName.Production;
30: if (env.IsDevelopment())
31: {
32: app.UseDeveloperExceptionPage();
33: }
34: else
35: {
36: app.UseExceptionHandler("/error");
37: }
38: #endregion
39:
40: #if StatusCodePages
41: #region snippet_StatusCodePages
42: app.UseStatusCodePages(async context =>
43: {
44: context.HttpContext.Response.ContentType = "text/plain";
45: await context.HttpContext.Response.WriteAsync(
46: "Status code page, status code: " +
47: context.HttpContext.Response.StatusCode);
48: });
49: #endregion
50: #endif
51: #if StatusCodePagesWithRedirect
52: #region snippet_StatusCodePagesWithRedirect
53: app.UseStatusCodePagesWithRedirects("/error/{0}");
54: #endregion
55: #endif
56:
57: app.MapWhen(context => context.Request.Path == "/missingpage", builder => { });
58:
59: // "/error/400"
60: app.Map("/error", error =>
61: {
62: error.Run(async context =>
63: {
64: var builder = new StringBuilder();
65: builder.AppendLine("<html><body>");
66: builder.AppendLine("An error occurred.<br />");
67: var path = context.Request.Path.ToString();
68: if (path.Length > 1)
69: {
70: builder.AppendLine("Status Code: " +
71: HtmlEncoder.Default.Encode(path.Substring(1)) + "<br />");
72: }
73: var referrer = context.Request.Headers["referer"];
74: if (!string.IsNullOrEmpty(referrer))
75: {
76: builder.AppendLine("Return to <a href=\"" +
77: HtmlEncoder.Default.Encode(referrer) + "\">" +
78: WebUtility.HtmlEncode(referrer) + "</a><br />");
79: }
80: builder.AppendLine("</body></html>");
81: context.Response.ContentType = "text/html";
82: await context.Response.WriteAsync(builder.ToString());
83: });
84: });
85: #region snippet_AppRun
86: app.Run(async (context) =>
87: {
88: if (context.Request.Query.ContainsKey("throw"))
89: {
90: throw new Exception("Exception triggered!");
91: }
92: var builder = new StringBuilder();
93: builder.AppendLine("<html><body>Hello World!");
94: builder.AppendLine("<ul>");
95: builder.AppendLine("<li><a href=\"/?throw=true\">Throw Exception</a></li>");
96: builder.AppendLine("<li><a href=\"/missingpage\">Missing Page</a></li>");
97: builder.AppendLine("</ul>");
98: builder.AppendLine("</body></html>");
99:
100: context.Response.ContentType = "text/html";
101: await context.Response.WriteAsync(builder.ToString());
102: });
103: #endregion
104: }
105: }
106: }
Put UseDeveloperExceptionPage
before any middleware you want to catch exceptions in, such as app.UseMvc
.
[!WARNING] Enable the developer exception page only when the app is running in the Development environment. You don???t want to share detailed exception information publicly when the app runs in production. Learn more about configuring environments.
To see the developer exception page, run the sample application with the environment set to Development
, and add ?throw=true
to the base URL of the app. The page includes several tabs with information about the exception and the request. The first tab includes a stack trace.
The next tab shows the query string parameters, if any.
This request didn???t have any cookies, but if it did, they would appear on the Cookies tab. You can see the headers that were passed in the last tab.
Configuring a custom exception handling page
It???s a good idea to configure an exception handler page to use when the app is not running in the Development
environment.
[!code-csharpMain]
1: #define StatusCodePages // or StatusCodePagesWithRedirect
2:
3: using System;
4: using System.Collections.Generic;
5: using System.Linq;
6: using System.Threading.Tasks;
7: using Microsoft.AspNetCore.Builder;
8: using Microsoft.AspNetCore.Hosting;
9: using Microsoft.AspNetCore.Http;
10: using Microsoft.Extensions.DependencyInjection;
11: using Microsoft.Extensions.Logging;
12: using System.Text;
13: using System.Text.Encodings.Web;
14: using System.Net;
15: using Microsoft.AspNetCore.Diagnostics;
16:
17: namespace ErrorHandlingSample
18: {
19: public class Startup
20: {
21: public void ConfigureServices(IServiceCollection services)
22: {
23: }
24:
25: #region snippet_DevExceptionPage
26: public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)
27: {
28: loggerFactory.AddConsole();
29: env.EnvironmentName = EnvironmentName.Production;
30: if (env.IsDevelopment())
31: {
32: app.UseDeveloperExceptionPage();
33: }
34: else
35: {
36: app.UseExceptionHandler("/error");
37: }
38: #endregion
39:
40: #if StatusCodePages
41: #region snippet_StatusCodePages
42: app.UseStatusCodePages(async context =>
43: {
44: context.HttpContext.Response.ContentType = "text/plain";
45: await context.HttpContext.Response.WriteAsync(
46: "Status code page, status code: " +
47: context.HttpContext.Response.StatusCode);
48: });
49: #endregion
50: #endif
51: #if StatusCodePagesWithRedirect
52: #region snippet_StatusCodePagesWithRedirect
53: app.UseStatusCodePagesWithRedirects("/error/{0}");
54: #endregion
55: #endif
56:
57: app.MapWhen(context => context.Request.Path == "/missingpage", builder => { });
58:
59: // "/error/400"
60: app.Map("/error", error =>
61: {
62: error.Run(async context =>
63: {
64: var builder = new StringBuilder();
65: builder.AppendLine("<html><body>");
66: builder.AppendLine("An error occurred.<br />");
67: var path = context.Request.Path.ToString();
68: if (path.Length > 1)
69: {
70: builder.AppendLine("Status Code: " +
71: HtmlEncoder.Default.Encode(path.Substring(1)) + "<br />");
72: }
73: var referrer = context.Request.Headers["referer"];
74: if (!string.IsNullOrEmpty(referrer))
75: {
76: builder.AppendLine("Return to <a href=\"" +
77: HtmlEncoder.Default.Encode(referrer) + "\">" +
78: WebUtility.HtmlEncode(referrer) + "</a><br />");
79: }
80: builder.AppendLine("</body></html>");
81: context.Response.ContentType = "text/html";
82: await context.Response.WriteAsync(builder.ToString());
83: });
84: });
85: #region snippet_AppRun
86: app.Run(async (context) =>
87: {
88: if (context.Request.Query.ContainsKey("throw"))
89: {
90: throw new Exception("Exception triggered!");
91: }
92: var builder = new StringBuilder();
93: builder.AppendLine("<html><body>Hello World!");
94: builder.AppendLine("<ul>");
95: builder.AppendLine("<li><a href=\"/?throw=true\">Throw Exception</a></li>");
96: builder.AppendLine("<li><a href=\"/missingpage\">Missing Page</a></li>");
97: builder.AppendLine("</ul>");
98: builder.AppendLine("</body></html>");
99:
100: context.Response.ContentType = "text/html";
101: await context.Response.WriteAsync(builder.ToString());
102: });
103: #endregion
104: }
105: }
106: }
In an MVC app, don???t explicitly decorate the error handler action method with HTTP method attributes, such as HttpGet
. Using explicit verbs could prevent some requests from reaching the method.
Configuring status code pages
By default, your app will not provide a rich status code page for HTTP status codes such as 500 (Internal Server Error) or 404 (Not Found). You can configure the StatusCodePagesMiddleware
by adding a line to the Configure
method:
By default, this middleware adds simple, text-only handlers for common status codes, such as 404:
The middleware supports several different extension methods. One takes a lambda expression, another takes a content type and format string.
[!code-csharpMain]
1: #define StatusCodePages // or StatusCodePagesWithRedirect
2:
3: using System;
4: using System.Collections.Generic;
5: using System.Linq;
6: using System.Threading.Tasks;
7: using Microsoft.AspNetCore.Builder;
8: using Microsoft.AspNetCore.Hosting;
9: using Microsoft.AspNetCore.Http;
10: using Microsoft.Extensions.DependencyInjection;
11: using Microsoft.Extensions.Logging;
12: using System.Text;
13: using System.Text.Encodings.Web;
14: using System.Net;
15: using Microsoft.AspNetCore.Diagnostics;
16:
17: namespace ErrorHandlingSample
18: {
19: public class Startup
20: {
21: public void ConfigureServices(IServiceCollection services)
22: {
23: }
24:
25: #region snippet_DevExceptionPage
26: public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)
27: {
28: loggerFactory.AddConsole();
29: env.EnvironmentName = EnvironmentName.Production;
30: if (env.IsDevelopment())
31: {
32: app.UseDeveloperExceptionPage();
33: }
34: else
35: {
36: app.UseExceptionHandler("/error");
37: }
38: #endregion
39:
40: #if StatusCodePages
41: #region snippet_StatusCodePages
42: app.UseStatusCodePages(async context =>
43: {
44: context.HttpContext.Response.ContentType = "text/plain";
45: await context.HttpContext.Response.WriteAsync(
46: "Status code page, status code: " +
47: context.HttpContext.Response.StatusCode);
48: });
49: #endregion
50: #endif
51: #if StatusCodePagesWithRedirect
52: #region snippet_StatusCodePagesWithRedirect
53: app.UseStatusCodePagesWithRedirects("/error/{0}");
54: #endregion
55: #endif
56:
57: app.MapWhen(context => context.Request.Path == "/missingpage", builder => { });
58:
59: // "/error/400"
60: app.Map("/error", error =>
61: {
62: error.Run(async context =>
63: {
64: var builder = new StringBuilder();
65: builder.AppendLine("<html><body>");
66: builder.AppendLine("An error occurred.<br />");
67: var path = context.Request.Path.ToString();
68: if (path.Length > 1)
69: {
70: builder.AppendLine("Status Code: " +
71: HtmlEncoder.Default.Encode(path.Substring(1)) + "<br />");
72: }
73: var referrer = context.Request.Headers["referer"];
74: if (!string.IsNullOrEmpty(referrer))
75: {
76: builder.AppendLine("Return to <a href=\"" +
77: HtmlEncoder.Default.Encode(referrer) + "\">" +
78: WebUtility.HtmlEncode(referrer) + "</a><br />");
79: }
80: builder.AppendLine("</body></html>");
81: context.Response.ContentType = "text/html";
82: await context.Response.WriteAsync(builder.ToString());
83: });
84: });
85: #region snippet_AppRun
86: app.Run(async (context) =>
87: {
88: if (context.Request.Query.ContainsKey("throw"))
89: {
90: throw new Exception("Exception triggered!");
91: }
92: var builder = new StringBuilder();
93: builder.AppendLine("<html><body>Hello World!");
94: builder.AppendLine("<ul>");
95: builder.AppendLine("<li><a href=\"/?throw=true\">Throw Exception</a></li>");
96: builder.AppendLine("<li><a href=\"/missingpage\">Missing Page</a></li>");
97: builder.AppendLine("</ul>");
98: builder.AppendLine("</body></html>");
99:
100: context.Response.ContentType = "text/html";
101: await context.Response.WriteAsync(builder.ToString());
102: });
103: #endregion
104: }
105: }
106: }
There are also redirect extension methods. One sends a 302 status code to the client, and one returns the original status code to the client but also executes the handler for the redirect URL.
[!code-csharpMain]
1: #define StatusCodePages // or StatusCodePagesWithRedirect
2:
3: using System;
4: using System.Collections.Generic;
5: using System.Linq;
6: using System.Threading.Tasks;
7: using Microsoft.AspNetCore.Builder;
8: using Microsoft.AspNetCore.Hosting;
9: using Microsoft.AspNetCore.Http;
10: using Microsoft.Extensions.DependencyInjection;
11: using Microsoft.Extensions.Logging;
12: using System.Text;
13: using System.Text.Encodings.Web;
14: using System.Net;
15: using Microsoft.AspNetCore.Diagnostics;
16:
17: namespace ErrorHandlingSample
18: {
19: public class Startup
20: {
21: public void ConfigureServices(IServiceCollection services)
22: {
23: }
24:
25: #region snippet_DevExceptionPage
26: public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)
27: {
28: loggerFactory.AddConsole();
29: env.EnvironmentName = EnvironmentName.Production;
30: if (env.IsDevelopment())
31: {
32: app.UseDeveloperExceptionPage();
33: }
34: else
35: {
36: app.UseExceptionHandler("/error");
37: }
38: #endregion
39:
40: #if StatusCodePages
41: #region snippet_StatusCodePages
42: app.UseStatusCodePages(async context =>
43: {
44: context.HttpContext.Response.ContentType = "text/plain";
45: await context.HttpContext.Response.WriteAsync(
46: "Status code page, status code: " +
47: context.HttpContext.Response.StatusCode);
48: });
49: #endregion
50: #endif
51: #if StatusCodePagesWithRedirect
52: #region snippet_StatusCodePagesWithRedirect
53: app.UseStatusCodePagesWithRedirects("/error/{0}");
54: #endregion
55: #endif
56:
57: app.MapWhen(context => context.Request.Path == "/missingpage", builder => { });
58:
59: // "/error/400"
60: app.Map("/error", error =>
61: {
62: error.Run(async context =>
63: {
64: var builder = new StringBuilder();
65: builder.AppendLine("<html><body>");
66: builder.AppendLine("An error occurred.<br />");
67: var path = context.Request.Path.ToString();
68: if (path.Length > 1)
69: {
70: builder.AppendLine("Status Code: " +
71: HtmlEncoder.Default.Encode(path.Substring(1)) + "<br />");
72: }
73: var referrer = context.Request.Headers["referer"];
74: if (!string.IsNullOrEmpty(referrer))
75: {
76: builder.AppendLine("Return to <a href=\"" +
77: HtmlEncoder.Default.Encode(referrer) + "\">" +
78: WebUtility.HtmlEncode(referrer) + "</a><br />");
79: }
80: builder.AppendLine("</body></html>");
81: context.Response.ContentType = "text/html";
82: await context.Response.WriteAsync(builder.ToString());
83: });
84: });
85: #region snippet_AppRun
86: app.Run(async (context) =>
87: {
88: if (context.Request.Query.ContainsKey("throw"))
89: {
90: throw new Exception("Exception triggered!");
91: }
92: var builder = new StringBuilder();
93: builder.AppendLine("<html><body>Hello World!");
94: builder.AppendLine("<ul>");
95: builder.AppendLine("<li><a href=\"/?throw=true\">Throw Exception</a></li>");
96: builder.AppendLine("<li><a href=\"/missingpage\">Missing Page</a></li>");
97: builder.AppendLine("</ul>");
98: builder.AppendLine("</body></html>");
99:
100: context.Response.ContentType = "text/html";
101: await context.Response.WriteAsync(builder.ToString());
102: });
103: #endregion
104: }
105: }
106: }
If you need to disable status code pages for certain requests, you can do so:
var statusCodePagesFeature = context.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.
Also, be aware that once the headers for a response have been sent, you can???t change the response???s status code, nor can any exception pages or handlers run. The response must be completed or the connection aborted.
Server exception handling
In addition to the exception handling logic in your app, the server hosting your app performs some exception handling. If the server catches an exception before the headers are sent, the server sends a 500 Internal Server Error response with no body. If the server catches an exception after the headers have been sent, the server closes the connection. Requests that aren???t handled by your app are handled by the server. Any exception that occurs is handled by the server???s exception handling. Any configured custom error pages or exception handling middleware or filters don???t affect this behavior.
Startup exception handling
Only the hosting layer can handle exceptions that take place during app startup. You can configure how the host behaves in response to errors during startup using captureStartupErrors
and the detailedErrors
key.
Hosting can only show an error page for a captured startup error if the error occurs after host address/port binding. If any binding fails for any reason, the hosting layer logs a critical exception, the dotnet process crashes, and no error page is displayed.
ASP.NET MVC error handling
MVC apps have some additional options for handling errors, such as configuring exception filters and performing model validation.
Exception Filters
Exception filters can be configured globally or on a per-controller or per-action basis in an MVC app. These filters handle any unhandled exception that occurs during the execution of a controller action or another filter, and are not called otherwise. Learn more about exception filters in Filters.
[!TIP] Exception filters are good for trapping exceptions that occur within MVC actions, but they???re not as flexible as error handling middleware. Prefer middleware for the general case, and use filters only where you need to do error handling differently based on which MVC action was chosen.
Handling Model State Errors
Model validation occurs prior to each controller action being invoked, and it is the action method???s responsibility to inspect ModelState.IsValid
and react appropriately.
Some apps will choose to follow a standard convention for dealing with model validation errors, in which case a filter may be an appropriate place to implement such a policy. You should test how your actions behave with invalid model states. Learn more in Testing controller logic.
|