Skip to main content

Custom Error Pages in .htaccess

When something goes wrong on your website, Apache displays a plain, unstyled default error page. These generic pages offer no branding, no navigation, and no helpful guidance for your visitors. They make your site look unprofessional and often cause users to leave immediately.

Custom error pages let you replace those defaults with well-designed pages that match your site's look and feel, provide helpful information, and guide users back to working content. This guide covers how Apache handles errors by default, how to configure custom error pages using the ErrorDocument directive, which error codes matter most, and how to avoid common pitfalls that can hurt your SEO.

How Apache Handles Errors by Default

When Apache encounters an error, whether it is a missing page, a permissions issue, or a server failure, it returns an HTTP status code along with a basic HTML page. This default page contains only the status code, a brief description, and the Apache server signature.

For example, if a user requests a page that does not exist, they see something like this:

Not Found

The requested URL /missing-page was not found on this server.

Apache/2.4.57 (Ubuntu) Server at example.com Port 80

This default behavior has several problems:

  • No branding: The page does not match your site design, making it feel like the site is broken.
  • No navigation: Users have no way to get back to your site without manually editing the URL or hitting the back button.
  • Information leakage: The server signature reveals your Apache version and operating system, which attackers can use to find known vulnerabilities.
  • Poor user experience: A dead end with no guidance increases bounce rates and frustrates visitors.

Custom error pages solve all of these problems by letting you control exactly what users see when something goes wrong.

The ErrorDocument Directive

The ErrorDocument directive is the core tool for defining custom error responses. It is part of Apache's core module, so it is always available without needing to enable anything extra.

The syntax is:

ErrorDocument error-code action

Where:

  • error-code is the three-digit HTTP status code (e.g., 404, 500).
  • action can be one of three things: an inline text message, a local file path, or an external URL.

Inline Text Messages

The simplest approach is to provide a short text message enclosed in double quotes. Apache will display this text directly in the browser.

ErrorDocument 403 "Sorry, access to this resource is restricted."

When a 403 error occurs, the browser displays:

Sorry, access to this resource is restricted.
note

The opening double quote is part of the syntax and tells Apache that what follows is a text message rather than a URL. Apache renders the text as plain HTML, so you can include basic HTML tags inside the message if needed, though this is not recommended for anything beyond simple notices.

Inline messages are useful for quick setups or development environments, but they are not suitable for production because they lack styling, branding, and navigation.

Local File Paths

The most common and recommended approach is to point to a local HTML file on your server. The path must start with a forward slash (/) and is relative to your DocumentRoot.

ErrorDocument 404 /errors/404.html
ErrorDocument 500 /errors/500.html
ErrorDocument 403 /errors/403.html

If your DocumentRoot is /var/www/html, then Apache will serve the file at /var/www/html/errors/404.html when a 404 error occurs.

Here is an example of what a custom 404 page might look like:

/errors/404.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Page Not Found</title>
<style>
body {
font-family: Arial, sans-serif;
text-align: center;
padding: 50px;
background-color: #f5f5f5;
}
h1 { color: #333; font-size: 3rem; }
p { color: #666; font-size: 1.2rem; }
a { color: #0066cc; text-decoration: none; }
a:hover { text-decoration: underline; }
</style>
</head>
<body>
<h1>404</h1>
<p>The page you are looking for does not exist or has been moved.</p>
<p><a href="/">Go back to the homepage</a></p>
</body>
</html>
tip

Keep your custom error pages self-contained as much as possible. If your 500 error page relies on a PHP script, a database connection, or external assets that are also failing, the error page itself might fail to load. Use inline CSS and minimal dependencies for error pages.

External URLs

You can also redirect users to a full external URL when an error occurs:

ErrorDocument 404 https://example.com/custom-404-handler

When a 404 error occurs, Apache sends a 302 redirect to the specified URL. The browser then makes a new request to that URL.

There is a critical problem with this approach. Because the browser receives a 302 redirect followed by a 200 OK from the new page, the original 404 status code is lost. Search engines see a 302 and then a 200, which means they think the error page is actual content. This creates soft 404 issues (covered in detail later in this guide).

warning

Avoid using external URLs for ErrorDocument unless you have a very specific reason. The redirect changes the HTTP status code that search engines and browsers see, which can cause significant SEO problems. Always prefer local file paths.

Common Mistake: Wrong Path Format

Wrong approach:

# Relative path without leading slash
ErrorDocument 404 errors/404.html

Apache interprets paths without a leading slash as text messages, not file paths. The user would see the literal text errors/404.html displayed in their browser instead of the actual HTML page.

Correct approach:

# Absolute path with leading slash
ErrorDocument 404 /errors/404.html

Common Error Codes

While HTTP defines dozens of status codes, only a handful are commonly encountered by website visitors. Here are the ones you should create custom pages for.

400 Bad Request

The server cannot process the request because of something the client sent that is malformed or invalid. This can happen due to corrupted cookies, oversized headers, or malformed URLs.

ErrorDocument 400 /errors/400.html

Your 400 page should suggest:

  • Clearing browser cookies and cache.
  • Checking the URL for typos or special characters.
  • Trying the request again.

401 Unauthorized

The requested resource requires authentication, and the user has either not provided credentials or provided invalid ones. This typically appears when using HTTP Basic or Digest authentication.

ErrorDocument 401 /errors/401.html

Your 401 page should:

  • Explain that the resource requires login credentials.
  • Provide a link to the login page if applicable.
  • Avoid revealing what the protected resource contains.

403 Forbidden

The server understood the request but refuses to authorize it. Unlike 401, providing credentials will not help. The server has decided this client is not allowed to access the resource.

ErrorDocument 403 /errors/403.html

Common causes include:

  • Directory listing is disabled and no index file exists.
  • File permissions are set incorrectly.
  • IP-based access restrictions block the visitor.
  • The .htaccess file explicitly denies access.

Your 403 page should politely inform the user that access is restricted without revealing why.

404 Not Found

The most common error on the web. The server cannot find the requested resource. This happens when pages are deleted, URLs are mistyped, or links from external sites point to pages that no longer exist.

ErrorDocument 404 /errors/404.html

A good 404 page should:

  • Clearly state that the page was not found.
  • Provide a search box if your site has search functionality.
  • Link to the homepage and key sections of the site.
  • Optionally suggest popular or related pages.

410 Gone

Similar to 404, but with a stronger signal: the resource used to exist but has been intentionally and permanently removed. Unlike a 404 (which could be temporary), a 410 tells search engines to remove the URL from their index.

ErrorDocument 410 /errors/410.html

Use 410 when you deliberately remove content and never intend to bring it back. This is especially useful for:

  • Discontinued products.
  • Expired promotions or events.
  • Content removed for legal reasons.

Your 410 page should explain that the content has been permanently removed and suggest alternatives.

500 Internal Server Error

A generic server-side error indicating something went wrong while processing the request. Common causes include syntax errors in .htaccess, PHP fatal errors, broken CGI scripts, or misconfigured server settings.

ErrorDocument 500 /errors/500.html
caution

Your 500 error page should be a static HTML file, not a PHP script. If PHP itself is the cause of the 500 error, a PHP-based error page will also fail. Use plain HTML with inline CSS and avoid any server-side processing.

Your 500 page should:

  • Apologize for the inconvenience.
  • Assure the user the problem is being investigated.
  • Suggest trying again in a few minutes.
  • Provide a way to contact support if the issue persists.

Complete Error Page Configuration

Here is a comprehensive setup covering all common error codes:

Complete custom error pages
# Client Errors
ErrorDocument 400 /errors/400.html
ErrorDocument 401 /errors/401.html
ErrorDocument 403 /errors/403.html
ErrorDocument 404 /errors/404.html
ErrorDocument 410 /errors/410.html

# Server Errors
ErrorDocument 500 /errors/500.html
ErrorDocument 502 /errors/502.html
ErrorDocument 503 /errors/503.html
ErrorDocument 504 /errors/504.html

Preventing Common Errors

Beyond displaying custom error pages, you can proactively prevent certain errors from occurring in the first place.

Avoiding 404s with Rewrites

If you have restructured your site or renamed files, old URLs will return 404 errors. Instead of letting visitors hit dead ends, use rewrite rules to silently map old URLs to new ones.

Preventing 404s through rewrites
<IfModule mod_rewrite.c>
RewriteEngine On

# Old blog URL structure to new structure
RewriteRule ^blog/post/([0-9]+)$ /articles/$1 [R=301,L]

# Renamed pages
RewriteRule ^about-us\.html$ /about [R=301,L]
RewriteRule ^services\.php$ /what-we-do [R=301,L]
</IfModule>

For large-scale migrations where hundreds of URLs have changed, you can use a RewriteMap (in server config, not .htaccess) or generate individual redirect rules programmatically.

Graceful Fallbacks

You can use mod_rewrite conditions to serve fallback content instead of errors in various scenarios.

Serving a Default Image When the Requested Image Is Missing

Image fallback
<IfModule mod_rewrite.c>
RewriteEngine On

# If an image file doesn't exist, serve a placeholder
RewriteCond %{REQUEST_FILENAME} !-f
RewriteRule \.(jpg|jpeg|png|gif|webp)$ /images/placeholder.png [L]
</IfModule>

Instead of a broken image icon, visitors see a branded placeholder image.

Disabling MultiViews to Prevent Unexpected 404s

Apache's MultiViews feature (part of content negotiation) can cause unexpected behavior. When enabled, if a user requests /some/path/foo and that exact file does not exist, Apache scans the directory for files named foo.* and tries to serve the best match. This can lead to surprising results or 404 errors when the matching logic fails.

# Disable MultiViews to prevent content negotiation issues
Options -MultiViews

Why does this matter? Consider this scenario: you have a file called /about.php and you are using clean URL rewrites. With MultiViews enabled, a request to /about might be handled by MultiViews (which finds about.php) before your rewrite rules run, causing conflicts and unexpected behavior. Disabling MultiViews ensures your rewrite rules have full control.

Recommended setup for clean URL sites
Options -MultiViews

<IfModule mod_rewrite.c>
RewriteEngine On

RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule ^(.*)$ /index.php [L]
</IfModule>

Fallback for Missing Pages in Subdirectories

If you have a blog section and want missing blog posts to redirect to the blog index instead of showing a 404:

Blog fallback
<IfModule mod_rewrite.c>
RewriteEngine On

# If a blog post doesn't exist, redirect to blog index
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule ^blog/(.+)$ /blog/ [R=302,L]
</IfModule>
note

Use a 302 (temporary) redirect for fallbacks, not 301. The missing content might be added later, and you do not want browsers or search engines to permanently cache the redirect to the index page.

Soft 404s and SEO Implications

A soft 404 occurs when a page that does not exist returns a 200 OK status code instead of a proper 404 status code. Search engines detect these and flag them as problems because the content of the page clearly indicates an error even though the HTTP status says everything is fine.

What Causes Soft 404s

There are several common causes:

1. Using external URL redirects for error pages:

# BAD: This causes a 302 redirect, then a 200 response
ErrorDocument 404 https://example.com/not-found

The flow is: client requests missing page, gets a 302 redirect, follows it to /not-found, and receives a 200 OK. The 404 status is lost entirely.

2. Front controller returning 200 for unknown routes:

# The .htaccess correctly routes to index.php
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule ^(.*)$ /index.php [L]

If index.php does not explicitly set a 404 status code for unknown routes, every non-existent URL appears as valid content to search engines.

3. Custom 404 page served without the proper status code:

If your error page is a PHP script that outputs content but forgets to send the correct header:

Wrong: Missing status header
<?php
// This file is served via ErrorDocument 404, but...
// No explicit header means the ErrorDocument mechanism handles it.
// If called directly via rewrite instead of ErrorDocument, it returns 200!
echo "<h1>Page Not Found</h1>";

How to Avoid Soft 404s

Use local file paths with ErrorDocument (recommended):

# CORRECT: Apache sends the 404 status code automatically
ErrorDocument 404 /errors/404.html

When you use a local path with ErrorDocument, Apache automatically sends the correct 404 status code along with the content of the error page. This is the simplest and most reliable approach.

Set the status header explicitly in dynamic error pages:

If you must use a PHP file as your error page, explicitly set the status code:

Correct: Explicit 404 header in PHP
<?php
http_response_code(404);
?>
<!DOCTYPE html>
<html lang="en">
<head>
<title>Page Not Found</title>
</head>
<body>
<h1>404 - Page Not Found</h1>
<p>The page you are looking for does not exist.</p>
<p><a href="/">Return to homepage</a></p>
</body>
</html>

Ensure your front controller handles unknown routes properly:

Correct: Front controller with proper 404 handling
<?php
$route = $_SERVER['REQUEST_URI'];

$validRoutes = ['/about', '/contact', '/blog'];

if (!in_array($route, $validRoutes)) {
http_response_code(404);
include 'errors/404.html';
exit;
}

// ... handle valid routes

Verifying Your Error Pages

You can verify that your error pages return the correct status code using curl:

curl -I https://example.com/this-page-does-not-exist

The response should include:

HTTP/1.1 404 Not Found

If you see HTTP/1.1 200 OK or HTTP/1.1 302 Found, your error handling is misconfigured and creating soft 404s.

warning

Google Search Console reports soft 404s under the Coverage or Pages report. If you see a growing number of soft 404s, check your ErrorDocument configuration and your application's route handling. Unresolved soft 404s waste your crawl budget, meaning search engines spend time crawling error pages instead of your actual content.

Quick Summary of SEO-Safe Error Handling

ApproachHTTP Status Seen by Search EngineSEO Safe?
ErrorDocument 404 /errors/404.html404Yes
ErrorDocument 404 "Not found"404Yes
ErrorDocument 404 https://example.com/404302, then 200No
Front controller without 404 header200No
PHP error page with http_response_code(404)404Yes

The golden rule is straightforward: always ensure that non-existent pages return a genuine 404 (or 410) HTTP status code. Use ErrorDocument with local file paths, avoid external URL redirects for error pages, and make sure your application explicitly sets the correct status code for routes that do not exist.