WordPress handles errors like it is 2005. Permission denied? White screen with black text. Expired nonce? Same white screen. Fatal PHP error? Literal white screen of death. Database connection failed? A page that looks like it was designed by someone who had never visited a website.
These screens are the first thing users see when something goes wrong. And something always goes wrong. A plugin update breaks. A session expires mid-form. Your visitors will see an error page. The only variable is whether it looks like it belongs to your brand or like a computer science textbook from 1998.
I built Graceful Error Pages to fix this. It is now live in the WordPress.org plugin directory. It intercepts WordPress error screens, both wp_die() and PHP fatal errors, and replaces them with branded, professional pages. It ships with five templates and auto-detects your site name, logo, and brand colors on activation.
Why every error page plugin only fixes 404
Search for "WordPress custom error page plugin" and you will find dozens of options. Smart Custom 404 Error Page. SeedProd 404 Page. Starter Templates 404. They all solve the same problem: replacing the 404 Not Found page with something branded.
But 404 is one error. WordPress has an entire category of error screens that no plugin touches. A user's session expires mid-form and they see a raw wp_die() screen. A comment submission fails, same screen. A plugin triggers a permission error, same screen. PHP crashes and they get the white screen of death.
Your visitors hit these errors on your site. And none of the popular error page plugins handle them because 404 pages are served by the theme system while wp_die() screens and fatal errors are served by separate WordPress subsystems.
The two error systems nobody thinks about
WordPress has two separate systems for displaying errors beyond the theme layer, and most developers only know about one of them.
The first is wp_die(). This is the function WordPress calls when it needs to stop execution and show an error. It handles strings, WP_Error objects, and any object that can be cast to a string. Plugins and themes that call wp_die() funnel through the same handler. WordPress itself calls it hundreds of times across its codebase for permission checks, nonce validation, capability enforcement, and input validation.
The second is PHP's fatal error handler. This is what kicks in when PHP itself crashes: uncaught exceptions, parse errors, out-of-memory conditions, and compile errors. WordPress added its own fatal error handler in 5.2, but it still produces a generic screen that tells the user nothing useful and does nothing to preserve trust.
Both systems produce pages that are identical: ugly, unbranded, and disconnected from your site's design. Graceful Error Pages intercepts both.
How the wp_die handler works
WordPress provides a filter called wp_die_handler that lets you replace the default handler function. The plugin hooks into this filter and routes all wp_die() calls through its own handler.
The handler does more than swap the template. It normalizes the incoming message, which can arrive as a string, a WP_Error object, or any stringifiable object. It resolves a smart title based on keyword detection in the error message. If the message contains "expired," the title becomes "Link Expired." If it contains "permission," the title becomes "Access Denied." This means the error page always has a human-readable heading instead of the generic "Something went wrong."
The handler validates status codes to the 100-599 HTTP range, sets cache-prevention headers so proxies and CDNs skip the error page, and renders the page through the template engine with the admin's configured branding.
The handler includes guard conditions. It skips rendering for REST API requests, AJAX calls, WP-CLI execution, cron jobs, and the Customizer preview. An API consumer expects a JSON error response. A cron job expects a log entry. Returning styled HTML there would break integrations, so the handler returns raw error data instead.
Fatal errors are a different problem
The wp_die() handler covers controlled errors. Fatal errors are uncontrolled. When PHP encounters an uncaught exception, a parse error, or runs out of memory, it does not call a handler function. It crashes. Output buffers may be partially filled. The filesystem may be in an unreliable state. WordPress's own enqueue system is probably not available.
The plugin handles this with register_shutdown_function(), which PHP calls after script execution ends, including after fatal errors. On every request, the plugin reserves 32KB of memory. If PHP runs out of memory, that reserved buffer provides enough space to render the error page instead of producing a blank white screen.
The fatal error template is self-contained. All CSS is inline. No external files are loaded. No WordPress functions are called for rendering because WordPress itself may be the thing that crashed. The template is a single block of HTML that the shutdown function outputs directly.
The shutdown function detects E_ERROR, E_PARSE, E_COMPILE_ERROR, and E_CORE_ERROR. It clears any partial output buffers so the user does not see half-rendered HTML followed by an error page. And it checks whether the request expects JSON, in which case it returns structured error data instead of HTML.
Auto-detection and zero configuration
The plugin works the moment you activate it. No setup wizard. No configuration screens to fill out before it does anything. On activation, it reads your site name from get_bloginfo(), your logo from the custom logo theme mod, your site icon, and your brand color from the Customizer. If no custom color is set, it defaults to a standard blue.
For the majority of WordPress sites that have a logo and site name configured, the error pages are branded without manual intervention. The admin settings page exists for fine-tuning: choosing one of the five templates (Minimal, Corporate, Friendly, Dark, Starter), adjusting colors, customizing the error message text, adding support links, or enabling dark mode. But none of that is required.
The merge tag system supports four dynamic values: {site_name}, {year}, {home_url}, and {back_url}. These work in error titles, messages, and button labels. The admin UI includes autocomplete for the tags so you do not need to memorize them.
Template security
I spent more time on the template engine than I expected. Templates are PHP files that receive data through a controlled scope. The engine validates template paths to prevent directory traversal attacks. It blocks relative path components, null bytes, and verifies via realpath() that the resolved path stays within the templates directory.
The engine replaces merge tags in two stages: first in the context values before they reach the template, then in the rendered HTML output with esc_html() escaping. This prevents injection through tag values. Non-developers can still use merge tags in the settings screen without touching code.
The live preview is an AJAX endpoint that renders the current template with the current settings in an iframe. The plugin secures it with nonce verification, capability checks, and X-Frame-Options: SAMEORIGIN headers. Combined with X-Robots-Tag: noindex, the preview is invisible to search engines and inaccessible to anyone without admin rights.
Why this is my second WordPress.org plugin
My first plugin on WordPress.org was SampleHQ Request Form, a much larger project with 909 unit tests and a full React form builder. Building that plugin taught me the WordPress.org review standards and the discipline of writing code that runs on servers you will never see.
Graceful Error Pages came from a specific pain point. While building SampleHQ as a multi-tenant SaaS on WordPress, I kept hitting error screens during development and testing. Permission denied errors during OAuth flows. Expired nonces during long configuration sessions. Fatal errors from plugin conflicts. All of them showed the same ugly default page.
For SampleHQ tenants, those error screens were a trust problem. A customer paying for a professional SaaS platform should never see a screen that looks like a broken server terminal. The error handling had to be part of the product experience. I treated it the same as any other UI surface.
I extracted the error handling code, generalized it to work on any WordPress site, added the template system and auto-detection, and packaged it as a standalone plugin. The result is 7 PHP classes, 5 templates, and 149 unit tests with 409 assertions. All output is escaped. All input is sanitized. All paths are validated. The plugin ships with declare(strict_types=1) in every file and passes PHPStan level 6 static analysis.
The plugin went through the WordPress.org review process and is now live in the plugin directory. The review went faster than my first submission. Most of the standards that tripped me up the first time, escaping, sanitization, prefixing, were already baked into how I write WordPress code now.
What it does not do
The plugin has zero runtime overhead on normal page loads. It does not add scripts, stylesheets, or database queries to your frontend. It only activates when an error actually occurs. It does not redirect, log errors, or send notifications. Those are separate concerns.
The plugin does one thing: when WordPress shows an error, it replaces the ugly default with a branded page. WordPress is 43% of the web. Most of those sites still show the default white screen when something breaks.
Try it
Graceful Error Pages is available on WordPress.org. Install it from your admin under Plugins, Add New, search for "Graceful Error Pages", and activate it. Your error pages will match your brand on the first activation. Customize them in Settings or leave the defaults.
The source code is on GitHub if you want to read the implementation or open an issue.
Top comments (0)