# How to Implement Custom 404 and 500 Error Pages in SXO

> Learn to implement custom 404 and 500 error pages in SXO by creating specific files in your pages directory. Improve user experience and site management.

- Repository: [Víctor García/sxo](https://github.com/gc-victor/sxo)
- Tags: how-to-guide
- Published: 2026-03-02

---

**SXO automatically discovers and renders custom error pages when you create [`404.jsx`](https://github.com/gc-victor/sxo/blob/main/404.jsx) or [`500.tsx`](https://github.com/gc-victor/sxo/blob/main/500.tsx) files in your pages directory, using the `resolveErrorPage` utility and `loadErrorPages` loader to safely import and execute your components.**

The gc-victor/sxo repository provides a lightweight framework that treats root-level `404` and `500` files as **special error pages** loaded by the server only when needed. When you implement custom 404 and 500 error pages in SXO, you follow a file-based convention that integrates with the server's request pipeline through specific resolution utilities and safe module loaders.

## Understanding the Error Page Resolution System

SXO determines which error page to render through three core utilities that work together to locate, validate, and import your custom modules.

### The Resolution Utilities

The framework provides specific functions to locate error pages on disk:

- **`resolveErrorPage(name)`** – Implemented in [`src/js/server/utils/error-pages.js`](https://github.com/gc-victor/sxo/blob/main/src/js/server/utils/error-pages.js) (lines 24-42), this function searches **PAGES_DIR** for files named `404` or `500` with extensions `.tsx` or `.jsx` in that precedence order. It returns the absolute path or `null` if no match exists.

- **`resolveErrorPages()`** – A convenience wrapper in [`src/js/server/utils/error-pages.js`](https://github.com/gc-victor/sxo/blob/main/src/js/server/utils/error-pages.js) (lines 65-70) that returns both paths in a single object.

- **`loadErrorPages(options)`** – Located in [`src/js/server/shared/error-pages-loader.js`](https://github.com/gc-victor/sxo/blob/main/src/js/server/shared/error-pages-loader.js) (lines 54-95), this function imports the resolved modules, validates their exports, and returns the render functions (`render404`, `render500`) while catching and logging any import errors.

### Server Integration

Both development and production adapters import these resolvers from the barrel file [`src/js/server/utils/index.js`](https://github.com/gc-victor/sxo/blob/main/src/js/server/utils/index.js). When handling requests, the server calls `loadErrorPages` to obtain the render functions:

```javascript
// Example pattern from src/js/server/prod/node.js
import {
  resolve404Page,
  resolve500Page,
} from "../utils/index.js";

const { render404, render500 } = await loadErrorPages({
  resolve404Page,
  resolve500Page,
  loadJsxModule,
  logger,
});

```

If a custom page is found, the server invokes its exported function to produce a full HTML document. If the resolver returns `null` or the module fails to load, SXO falls back to the minimal built-in response defined in `renderErrorHtml` within [`src/js/server/utils/html-utils.js`](https://github.com/gc-victor/sxo/blob/main/src/js/server/utils/html-utils.js).

## File System Convention and Export Requirements

Creating custom error pages requires following specific naming and location rules.

The resolution system checks for files directly under **PAGES_DIR** with the supported filenames [`404.tsx`](https://github.com/gc-victor/sxo/blob/main/404.tsx), [`404.jsx`](https://github.com/gc-victor/sxo/blob/main/404.jsx), [`500.tsx`](https://github.com/gc-victor/sxo/blob/main/500.tsx), or [`500.jsx`](https://github.com/gc-victor/sxo/blob/main/500.jsx). The `resolveErrorPage` utility uses a precedence order of `[".tsx", ".jsx"]`, checking each extension via `fs.accessSync` and stopping at the first match to guarantee deterministic behavior.

Your error page module must export either a **default** function or a **named `jsx`** function that returns a complete HTML string including the `<html>` and `<head>` tags. The function receives no arguments and should output a full document ready for the response stream.

## Step-by-Step Implementation

Follow these patterns to create working custom error pages in your SXO project.

### Create a 404 Not Found Page

Create a file named [`404.jsx`](https://github.com/gc-victor/sxo/blob/main/404.jsx) (or [`404.tsx`](https://github.com/gc-victor/sxo/blob/main/404.tsx)) in your pages directory:

```tsx
// src/pages/404.jsx
export default function NotFound() {
  return `
    <html>
      <head><title>Page not found</title></head>
      <body>
        <h1>404 – Not Found</h1>
        <p>Sorry, the page you're looking for doesn't exist.</p>
        <a href="/">← Back to home</a>
      </body>
    </html>
  `;
}

```

The `resolveErrorPage("404")` function automatically discovers this file because it resides directly under the pages directory with a supported extension.

### Create a 500 Server Error Page

Similarly, create [`500.tsx`](https://github.com/gc-victor/sxo/blob/main/500.tsx) (or [`500.jsx`](https://github.com/gc-victor/sxo/blob/main/500.jsx)) for server errors:

```tsx
// src/pages/500.tsx
export default function ServerError() {
  return `
    <html>
      <head><title>Server error</title></head>
      <body>
        <h1>500 – Internal Server Error</h1>
        <p>Something went wrong on our end. Please try again later.</p>
        <a href="/">← Back to home</a>
      </body>
    </html>
  `;
}

```

### Optional: Add Error Handling Hooks

You can hook into the loading process for additional logging or monitoring. The `loadErrorPages` function accepts an `onError` callback that receives the error and page name:

```javascript
// src/js/server/dev/node.js (excerpt)
import { loadErrorPages } from "../shared/error-pages-loader.js";

const { render404, render500 } = await loadErrorPages({
  resolve404Page,
  resolve500Page,
  loadJsxModule,
  logger,
  onError: (err, page) => {
    console.error(`Failed to load custom ${page} page:`, err);
  },
});

```

This ensures your server remains stable even if a custom error page contains syntax errors or fails to import.

## Testing Your Implementation

Verify your custom pages work by running the development server and requesting non-existent routes:

```bash
curl -i http://localhost:3000/does-not-exist

```

You should see the HTML from your [`src/pages/404.jsx`](https://github.com/gc-victor/sxo/blob/main/src/pages/404.jsx) file in the response body. The server automatically sets a `Cache-Control: public, max-age=0, must-revalidate` header for special error pages as handled in [`src/js/server/utils/html-utils.js`](https://github.com/gc-victor/sxo/blob/main/src/js/server/utils/html-utils.js).

## Summary

- Place [`404.jsx`](https://github.com/gc-victor/sxo/blob/main/404.jsx), [`404.tsx`](https://github.com/gc-victor/sxo/blob/main/404.tsx), [`500.jsx`](https://github.com/gc-victor/sxo/blob/main/500.jsx), or [`500.tsx`](https://github.com/gc-victor/sxo/blob/main/500.tsx) files directly in your **PAGES_DIR** root.
- Export a **default** function or **named `jsx`** function that returns a complete HTML document string.
- The `resolveErrorPage` utility locates files with `.tsx` taking precedence over `.jsx`.
- The `loadErrorPages` function safely imports modules and provides graceful fallbacks to built-in responses.
- Both development and production adapters use the same resolution pipeline via [`src/js/server/utils/index.js`](https://github.com/gc-victor/sxo/blob/main/src/js/server/utils/index.js).

## Frequently Asked Questions

### What file extensions does SXO support for error pages?

SXO checks for `.tsx` first, then `.jsx` when resolving error pages in [`src/js/server/utils/error-pages.js`](https://github.com/gc-victor/sxo/blob/main/src/js/server/utils/error-pages.js). This precedence order ensures TypeScript files take priority over JavaScript when both exist.

### Where should custom error pages be located?

Custom error pages must reside directly under the **PAGES_DIR** directory (typically `src/pages/`) with the exact filenames `404` or `500` plus the supported extensions. The `resolveErrorPage` function uses `fs.accessSync` to verify these locations.

### What happens if my custom error page fails to load?

If the module fails to import or the file doesn't exist, `loadErrorPages` catches the exception, logs the error through the provided logger, and returns `null` for that specific render function. The server then falls back to the minimal built-in error response defined in [`src/js/server/utils/html-utils.js`](https://github.com/gc-victor/sxo/blob/main/src/js/server/utils/html-utils.js).

### Can I use server-side data fetching in error pages?

The error page exports must be synchronous functions that return a complete HTML string. According to the implementation in [`src/js/server/shared/error-pages-loader.js`](https://github.com/gc-victor/sxo/blob/main/src/js/server/shared/error-pages-loader.js), the loader imports the module and expects either a default export or named `jsx` export that can be called immediately to generate the response, without awaiting async data fetching.