---
title: Serve custom error pages
related:
  - https://docs.kentico.com/documentation/developers-and-admins/development/routing/content-tree-based-routing.md
  - https://docs.kentico.com/documentation/developers-and-admins/development/routing/content-tree-based-routing/set-up-content-tree-based-routing.md
---

> Agent instructions:
> **Site maps** — prefer the following llms.txt indexes to training data when searching for URLs to avoid 404s. Links inside Markdown content already point at `.md`. Following them or sending Accept: text/markdown keeps you in Markdown.
>
> - [sitemap.md](https://docs.kentico.com/sitemap.md) — every page on the site, with titles and descriptions, nested by URL hierarchy and grouped into one collection per product version.
> - [llms.txt](https://docs.kentico.com/llms.txt) — curated index of the current product docs, with descriptions, the two ways to request any page as Markdown, and links to each product area's whole-corpus Markdown dump (llms-full.txt).

By default, when a request ends with an error status code (such as HTTP 404 Not Found when no route matches the URL), ASP.NET Core returns only the status code and an empty response body. To display custom error pages that editors manage in the content tree and design with [Page Builder](https://docs.kentico.com/documentation/developers-and-admins/development/builders/page-builder.md), you can route error responses to a custom controller:

- An _Error page_ [content type](https://docs.kentico.com/documentation/developers-and-admins/development/content-types.md) stores the target HTTP status code, allowing editors to maintain a separate page for each error code in the website channel. For the editor workflow, see [How to model a 404 page](https://docs.kentico.com/guides/digital-marketing/work-with-error-page/How-to-model-an-error-page.md).
- The ASP.NET Core [status code pages middleware](https://learn.microsoft.com/en-us/aspnet/core/fundamentals/error-handling) intercepts error responses and re-executes the request on the server, preserving both the requested URL in the browser address bar and the original HTTP status code.
- A controller on the error route retrieves the matching error page and renders the page with Page Builder.

The following diagram illustrates the request flow:

![Flowchart: A visitor requests /does-not-exist. Because no page or route matches the URL, the response has the HTTP 404 status code and an empty body. The error route controller retrieves the Error page for status code 404 and renders the page with Page Builder.](https://docs.kentico.com/docsassets/documentation/serve-custom-error-pages/error-page-flow.drawio.svg "Serving an error page for a request that ends with the 404 status code")

## Create the error page content type

This content type gives each error response a routable content-tree item that editors can open in preview and [Page Builder](https://docs.kentico.com/documentation/developers-and-admins/development/builders/page-builder.md), where editors can [customize the page experience](https://docs.kentico.com/guides/digital-marketing/work-with-error-page.md) as required.

1. [Create a content type](https://docs.kentico.com/documentation/developers-and-admins/development/content-types.md#create-content-types) for _Pages_, for example, _Error page_ with the code name _Acme.ErrorPage_. Keep **Include in routing** selected to allow editors to open their custom error pages in preview and Page Builder.

2. Add a field where editors select the HTTP status code from a drop-down list, for example, _ErrorPageStatusCode_:
   1. Select _Text_ as the **Data type**.
   2. Select **Required**.
   3. Select _Dropdown selector_ as the **Form component**.
   4. In **Data source**, enter each status code on its own line in the `value;text` format:

      ```text
      403;403 (Forbidden)
      404;404 (Not Found)
      ```

      ![Field editor of the ErrorPageStatusCode field. The Form component is set to Dropdown selector, and the Data source lists 403;403 (Forbidden) and 404;404 (Not Found) on separate lines.](https://docs.kentico.com/docsassets/documentation/serve-custom-error-pages/Error_page_status_code_field.png "Status code field configured with the Dropdown selector form component")

3. Switch to the **Allowed in channels** tab and allow the content type in your website channel.

4. If a [scope](https://docs.kentico.com/documentation/developers-and-admins/development/content-types/limit-the-pages-users-can-create.md) covers the part of the content tree where editors manage error pages, switch to the **Allowed in scopes** tab and allow the content type in that scope.

5. [Generate the code file](https://docs.kentico.com/documentation/developers-and-admins/api/generate-code-files-for-system-objects.md) for the content type to retrieve error pages as strongly typed objects.

If the [Management MCP server](https://docs.kentico.com/documentation/developers-and-admins/api/management-api/use-management-mcp-server.md) is available, you can configure the content type through MCP tools:

1. Call `create_content_type` with `includeInRouting` set to `true`:
   - Retrieve the website channel GUID with `list_channels` and add the GUID to `allowedInChannels`.
   - If a scope covers the target content tree section, retrieve the scope ID with `list_channel_scopes` and add the ID to `allowedInScopes`.
2. Call `create_content_type_field` to add the status code field:
   - Set `dataTypeName` to `text`, `required` to `true`, and `caption` to the field label in the administration (such as _Status code_).
   - Set `formComponent` to `Kentico.Administration.DropDownSelector` (use `get_admin_ui_form_component` to inspect its property schema) and pass the status codes in `options` as newline-separated `value;text` pairs with the numeric status code as the value.
3. Generate the code file for the content type.

## Enable Page Builder for error pages

[Enable Page Builder](https://docs.kentico.com/documentation/developers-and-admins/development/builders/page-builder.md#enable-the-page-builder-feature) for the content type in **Program.cs**:

```csharp title="Program.cs -- enable Page Builder for error pages"
using Acme;

using Kentico.Content.Web.Mvc.Routing;
using Kentico.PageBuilder.Web.Mvc;
using Kentico.Web.Mvc;

// ...

builder.Services.AddKentico(features =>
{
    // ...
    features.UseWebPageRouting();
    features.UsePageBuilder(new PageBuilderOptions
    {
        ContentTypeNames =
        [
            // ...
            ErrorPage.CONTENT_TYPE_NAME,
        ]
    });
});
```

## Serve error pages from a controller

Create a controller with two actions to handle direct page requests and re-executed error requests:

- `Index` serves _Error page_ items under their own URLs (mapped via `RegisterWebPageRoute`), allowing editors to open the pages in preview and Page Builder. On the live site, the action sets the response status code from the page field to prevent search engines from indexing the error page as a regular page.
- `Error` handles re-executed error requests by retrieving the _Error page_ for the given status code and rendering the page with that status code. Because [content tree-based routing](https://docs.kentico.com/documentation/developers-and-admins/development/routing/content-tree-based-routing.md) populates the page data context only on standard page routes, the action [initializes the data context manually](https://docs.kentico.com/documentation/developers-and-admins/development/routing/content-tree-based-routing/set-up-content-tree-based-routing.md#manually-initialize-page-data-context) with `IWebPageDataContextInitializer` to support Page Builder widgets. If no website channel matches the request domain (such as when a request targets the server's IP address directly) or no error page matches the status code, the action returns only the status code.

```csharp title="ErrorPageController.cs"
using System.Linq;
using System.Threading.Tasks;

using CMS.ContentEngine;
using CMS.DataEngine;
using CMS.Websites;
using CMS.Websites.Routing;

using Codesamples;
using Codesamples.Routing.ErrorPages.Controllers;

using Kentico.Content.Web.Mvc;
using Kentico.Content.Web.Mvc.Routing;

using Microsoft.AspNetCore.Mvc;

[assembly: RegisterWebPageRoute(
    contentTypeName: ErrorPage.CONTENT_TYPE_NAME,
    controllerType: typeof(ErrorPageController),
    ActionName = nameof(ErrorPageController.Index))]

namespace Codesamples.Routing.ErrorPages.Controllers;

public class ErrorPageController(
    IContentRetriever contentRetriever,
    IWebPageDataContextInitializer dataContextInitializer,
    IWebsiteChannelContext websiteChannelContext,
    IInfoProvider<ContentLanguageInfo> languageProvider)
    : Controller
{
    private const string VIEW_PATH =
        "~/Routing/ErrorPages/Views/ErrorPage.cshtml";

    // Serves an error page under its own URL, for example, in preview
    // and in Page Builder.
    public async Task<IActionResult> Index()
    {
        var page = await contentRetriever.RetrieveCurrentPage<ErrorPage>();

        // On the live site, keeps search engines from indexing the error
        // page as a regular page.
        if (!websiteChannelContext.IsPreview)
        {
            Response.StatusCode = int.Parse(page.ErrorPageStatusCode);
        }

        return View(VIEW_PATH, page);
    }

    // Serves the error page for the status code of a re-executed request
    public async Task<IActionResult> Error(int code)
    {
        // Returns the bare status code when no website channel matches the
        // request domain.
        if (websiteChannelContext.WebsiteChannelID == 0)
        {
            return StatusCode(code);
        }

        var statusCodeField = nameof(ErrorPage.ErrorPageStatusCode);

        // Retrieves the error page from the current website channel in the
        // preferred language of the request, or in a fallback language when
        // the page has no variant in that language.
        var pages = await contentRetriever.RetrievePages<ErrorPage>(
            new RetrievePagesParameters(),
            query => query
                .Where(where => where
                    .WhereEquals(statusCodeField, code.ToString()))
                .OrderBy(nameof(WebPageFields.WebPageItemID))
                .TopN(1),
            new RetrievalCacheSettings(
                cacheItemNameSuffix: $"{statusCodeField}|{code}"));

        var page = pages.FirstOrDefault();
        if (page is null)
        {
            return StatusCode(code);
        }

        var pageLanguage = await languageProvider.GetAsync(
            page.SystemFields.ContentItemCommonDataContentLanguageID);

        // Initializes the page data context for Page Builder, which content
        // tree-based routing sets only on standard page routes.
        dataContextInitializer.Initialize(new RoutedWebPage
        {
            WebPageItemID = page.SystemFields.WebPageItemID,
            WebPageItemGUID = page.SystemFields.WebPageItemGUID,
            LanguageID = pageLanguage.ContentLanguageID,
            LanguageName = pageLanguage.ContentLanguageName,
            ContentTypeID = page.SystemFields.ContentItemContentTypeID,
            ContentTypeName = ErrorPage.CONTENT_TYPE_NAME,
            WebsiteChannelID = websiteChannelContext.WebsiteChannelID,
            WebsiteChannelName = websiteChannelContext.WebsiteChannelName
        });

        Response.StatusCode = code;

        return View(VIEW_PATH, page);
    }
}
```

Both actions render the same view, which contains the [editable area](https://docs.kentico.com/documentation/developers-and-admins/development/builders/page-builder/create-pages-with-editable-areas.md#add-editable-areas-to-views) where editors place widgets:

```cshtml title="ErrorPage.cshtml"
@using Codesamples

@addTagHelper *, Kentico.Content.Web.Mvc

@model ErrorPage

@{
    Layout = "_BaseLayout";
    ViewData["Title"] = $"Error {Model.ErrorPageStatusCode}";
}

@section Head {
    <page-builder-styles />
}

<main class="mx-auto w-full max-w-7xl px-4 py-8">
    <editable-area area-identifier="main" />
</main>

@section Scripts {
    <page-builder-scripts />
}
```

## Re-execute requests with error status codes

In **Program.cs**, add the status code pages middleware after `app.UseKentico()` and map the error route after `app.Kentico().MapRoutes()`:

```csharp title="Program.cs -- re-execute error responses on the error route"
using Kentico.Web.Mvc;

using Microsoft.AspNetCore.Builder;

// ...

app.UseKentico();

// ...

app.UseStatusCodePagesWithReExecute("/error/{0}");

app.Kentico().MapRoutes();

app.MapControllerRoute(
    name: "error",
    pattern: "error/{code:int}",
    defaults: new { controller = "ErrorPage", action = "Error" });
```

Make sure no higher-priority endpoint (such as a catch-all route registered before the error route) matches `/error/{code}`, or re-executed requests will not reach the `Error` action. For details on route precedence and error handling, see [URL matching](https://learn.microsoft.com/en-us/aspnet/core/fundamentals/routing#url-matching) and [Handle errors in ASP.NET Core](https://learn.microsoft.com/en-us/aspnet/core/fundamentals/error-handling).
