Editor Friendly 500 Error Pages in Umbraco
A well-designed 500 error page can turn a frustrating experience into a reassuring one. But there’s a catch: 500 errors often occur when Umbraco itself is broken—database connection issues, configuration problems, or other server failures. If Umbraco can’t run, it can’t render your beautiful error page.
The solution? Pre-generate a static HTML version whenever editors publish the error page. That way, you have a fallback that works even when everything else is on fire.
The Strategy
We’ll create a two-layer approach:
- Dynamic route - A
/500URL that serves an editor-managed error page through Umbraco - Static fallback - An HTML file generated on publish, served when Umbraco is unavailable
This gives editors full control over the error page content while ensuring visitors always see something helpful, even during complete outages.
The Site Structure
Similar to the editor-friendly 404 page approach, we add a content picker to the site node:
- Property name: Error Page
- Alias:
errorPage - Type: Content Picker
Editors can then create a dedicated error page with helpful messaging, contact information, and status links.
The Content Finder
First, let’s create a content finder that handles the /500 route:
using Umbraco.Cms.Core.Routing;
using Umbraco.Cms.Core.Web;
using Umbraco.Cms.Core.Models.PublishedContent;
using Umbraco.Extensions;
public class ErrorPageContentFinder : IContentFinder
{
private readonly IUmbracoContextAccessor _umbracoContextAccessor;
public ErrorPageContentFinder(IUmbracoContextAccessor umbracoContextAccessor)
{
_umbracoContextAccessor = umbracoContextAccessor;
}
public Task<bool> TryFindContent(IPublishedRequestBuilder request)
{
var path = request.AbsolutePathDecoded.Trim('/');
if (!path.Equals("500", StringComparison.OrdinalIgnoreCase))
{
return Task.FromResult(false);
}
if (!_umbracoContextAccessor.TryGetUmbracoContext(out var umbracoContext))
{
return Task.FromResult(false);
}
var errorPage = FindErrorPage(umbracoContext);
if (errorPage is null)
{
return Task.FromResult(false);
}
request.SetPublishedContent(errorPage);
return Task.FromResult(true);
}
private IPublishedContent? FindErrorPage(IUmbracoContext umbracoContext)
{
var root = umbracoContext.Content?.GetAtRoot().FirstOrDefault();
if (root is null)
{
return null;
}
return root.Value<IPublishedContent>("errorPage");
}
}
Generating Static HTML on Publish
The key to our fallback strategy is generating a static HTML file whenever the error page is published. We use a notification handler that listens for publish events:
using Umbraco.Cms.Core.Events;
using Umbraco.Cms.Core.Notifications;
using Umbraco.Cms.Core.Models.PublishedContent;
using Umbraco.Cms.Core.Web;
using Umbraco.Extensions;
public class ErrorPagePublishNotificationHandler
: INotificationHandler<ContentPublishedNotification>
{
private readonly IUmbracoContextFactory _umbracoContextFactory;
private readonly IWebHostEnvironment _webHostEnvironment;
private readonly IHttpClientFactory _httpClientFactory;
private readonly ILogger<ErrorPagePublishNotificationHandler> _logger;
public ErrorPagePublishNotificationHandler(
IUmbracoContextFactory umbracoContextFactory,
IWebHostEnvironment webHostEnvironment,
IHttpClientFactory httpClientFactory,
ILogger<ErrorPagePublishNotificationHandler> logger)
{
_umbracoContextFactory = umbracoContextFactory;
_webHostEnvironment = webHostEnvironment;
_httpClientFactory = httpClientFactory;
_logger = logger;
}
public void Handle(ContentPublishedNotification notification)
{
using var contextReference = _umbracoContextFactory.EnsureUmbracoContext();
var umbracoContext = contextReference.UmbracoContext;
foreach (var content in notification.PublishedEntities)
{
// Check if this is an error page or if the site node's error page was updated
if (!IsErrorPageOrSiteNode(content, umbracoContext))
{
continue;
}
_ = GenerateStaticErrorPageAsync();
}
}
private bool IsErrorPageOrSiteNode(
Umbraco.Cms.Core.Models.IContent content,
IUmbracoContext umbracoContext)
{
var published = umbracoContext.Content?.GetById(content.Id);
if (published is null)
{
return false;
}
// Check if this content is referenced as an error page
var root = umbracoContext.Content?.GetAtRoot().FirstOrDefault();
var errorPage = root?.Value<IPublishedContent>("errorPage");
return errorPage?.Id == content.Id || root?.Id == content.Id;
}
private async Task GenerateStaticErrorPageAsync()
{
try
{
var client = _httpClientFactory.CreateClient();
// Fetch the rendered error page
var baseUrl = /* Get your site's base URL from configuration */;
var response = await client.GetStringAsync($"{baseUrl}/500");
// Save to wwwroot
var filePath = Path.Combine(
_webHostEnvironment.WebRootPath,
"errors",
"500.html");
var directory = Path.GetDirectoryName(filePath);
if (!Directory.Exists(directory))
{
Directory.CreateDirectory(directory!);
}
await File.WriteAllTextAsync(filePath, response);
_logger.LogInformation("Static 500 error page generated at {Path}", filePath);
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to generate static 500 error page");
}
}
}
Configuring the Error Page Fallback
In appsettings.json, configure the static file path as your error page:
{
"Umbraco": {
"CMS": {
"Hosting": {
"CustomErrors": {
"Mode": "RemoteOnly",
"Error500": "/errors/500.html"
}
}
}
}
}
For more control, you can also configure this in Program.cs:
app.UseStatusCodePagesWithReExecute("/errors/{0}.html");
Or handle it with custom middleware:
app.UseExceptionHandler(errorApp =>
{
errorApp.Run(async context =>
{
context.Response.StatusCode = 500;
context.Response.ContentType = "text/html";
var errorPagePath = Path.Combine(
app.Environment.WebRootPath,
"errors",
"500.html");
if (File.Exists(errorPagePath))
{
await context.Response.SendFileAsync(errorPagePath);
}
else
{
await context.Response.WriteAsync(
"<h1>An error occurred</h1><p>Please try again later.</p>");
}
});
});
Registering Everything
Register the content finder and notification handler in your composer:
using Umbraco.Cms.Core.Composing;
using Umbraco.Cms.Core.DependencyInjection;
using Umbraco.Cms.Core.Notifications;
public class ErrorPageComposer : IComposer
{
public void Compose(IUmbracoBuilder builder)
{
builder.ContentFinders().InsertBefore<ContentFinderByUrl, ErrorPageContentFinder>();
builder.AddNotificationHandler<ContentPublishedNotification,
ErrorPagePublishNotificationHandler>();
}
}
How It Works
Normal operation:
- A server error occurs
- The exception handler middleware catches it
- It serves the static
/errors/500.htmlfile - Visitors see a friendly, editor-managed error page
When editors update the error page:
- Editor publishes changes to the error page
- The notification handler detects the change
- It fetches the rendered page from
/500 - It saves the HTML to
/errors/500.html - Future errors automatically show the updated content
Complete outage:
- Database is down, Umbraco can’t start
- Static file middleware still works
- The pre-generated
500.htmlis served - Visitors still see a helpful error page
Why Static HTML?
The whole point of a 500 error page is to handle situations where something has gone seriously wrong. If the database is down, Umbraco can’t render anything. If there’s a code exception in startup, the pipeline never gets configured.
By pre-generating static HTML, you have a fallback that:
- Requires no database - Pure file system access
- Requires no code execution - Just static file serving
- Survives deployment issues - The file persists through deploys
- Stays in sync - Automatically regenerated on publish
Final Thoughts
A thoughtful error page shows users you care about their experience, even when things go wrong. By combining Umbraco’s content management capabilities with a static HTML fallback, you give editors full control while ensuring resilience during outages.
This pairs well with the editor-friendly 404 page approach—together, they give you a complete error handling strategy that keeps both editors and visitors happy.