Error Pages
Custom _404.tsx and _500.tsx pages — file-based, layout-aware, and fully typed with error props.
Overview
Place _404.tsx and _500.tsx directly in app/pages/. They are standard server components and support everything regular pages do: layouts, useHtml(), client components, and HMR in dev. The underscore prefix tells NukeJS these are reserved error pages — they are excluded from routing, so /_404 and /_500 are never reachable as URLs.
_404.tsx — Page Not Found
Rendered whenever no route matches the requested URL. NukeJS automatically responds with a 404 HTTP status code.
import { useHtml } from 'nukejs'
import { Link } from 'nukejs'
export default function NotFound() {
useHtml({ title: 'Page Not Found' })
return (
<main>
<h1>404 — Page Not Found</h1>
<p>The page you're looking for doesn't exist.</p>
<Link href="/">Go home</Link>
</main>
)
}layout.tsx is applied to both _404.tsx and _500.tsx, so your nav and footer appear automatically — no extra wiring needed.Preventing layout loops
Because error pages inherit the root layout, a bug inside layout.tsx — or any component it renders — will cause an infinite loop: the layout throws, NukeJS tries to render the error page, the layout throws again, and so on.
Guard against this by wrapping every non-essential layout component in a try / catch. If the component throws, return only children so the page content (including the error page) still renders:
import Component from "../component"
export default function Layout({ children }: { children: React.ReactNode }) {
try {
return (
<>
<div>{children}</div>
<div><Component /></div>
</>
)
} catch {
// Component (or the layout itself) threw — fall back to bare children
// so error pages can render without triggering another error cycle.
return <>{children}</>
}
}_500.tsx — Internal Server Error
Rendered when a page handler throws an unhandled error. NukeJS automatically forwards error details as props so you can surface them — especially useful during development.
import { useHtml } from 'nukejs'
import { Link } from 'nukejs'
interface ErrorProps {
errorMessage?: string // human-readable error description
errorStatus?: string // HTTP status code if set on the thrown error
errorStack?: string // stack trace — only populated in development
}
export default function ServerError({ errorMessage, errorStack }: ErrorProps) {
useHtml({ title: 'Something went wrong' })
return (
<main>
<h1>500 — Server Error</h1>
<p>Something went wrong on our end. Please try again.</p>
{errorMessage && <p><strong>{errorMessage}</strong></p>}
{errorStack && <pre>{errorStack}</pre>}
<Link href="/">Go home</Link>
</main>
)
}errorStack is dev-only The stack trace is only populated when NODE_ENV !== 'production'. In production, errorStack is always undefined, so the <pre> block won't render — no accidental leaks.Server errors
Any unhandled throw inside a server page component — including async data fetching — routes to _500.tsx. The error message and stack trace are forwarded as props automatically:
export default async function Dashboard() {
const data = await fetchData() // throws → _500.tsx is rendered
return <main>{data.name}</main>
}Attach a status property to a thrown error to control the HTTP status code sent with the response. The value is forwarded to _500.tsx as the errorStatus prop:
export default async function Post({ slug }: { slug: string }) {
const post = await db.getPost(slug)
if (!post) {
const err = new Error('Post not found')
;(err as any).status = 404
throw err // _500.tsx receives errorMessage="Post not found", errorStatus="404"
}
return <article>{post.title}</article>
}Client errors
Unhandled errors in client components and async code are automatically caught and routed to _500.tsx via an in-place SPA navigation — no full page reload. Three mechanisms cover all cases:
| Mechanism | What it catches |
|---|---|
| React Error Boundary | Render and lifecycle errors in every "use client" component |
window.onerror | Synchronous throws in event handlers and other non-React code |
window.onunhandledrejection | Unhandled Promise rejections from async functions |
"use client"
export default function FaultyButton() {
const handleClick = () => {
throw new Error('Something broke!') // caught by window.onerror → _500.tsx
}
return <button onClick={handleClick}>Click me</button>
}"use client"
import { useEffect } from 'react'
export default function FaultyFetch() {
useEffect(() => {
// Unhandled rejection → caught by window.onunhandledrejection → _500.tsx
fetch('/api/broken').then(res => {
if (!res.ok) throw new Error(`API error ${res.status}`)
})
}, [])
return <div>Loading...</div>
}The _500.tsx page receives errorMessage and errorStack props from client errors just like server errors, so a single error page handles both origins consistently.
Errors in API routes
API routes in server/ don't use the error pages — they respond directly. Use res.json() with the appropriate status code:
import type { ApiRequest, ApiResponse } from 'nukejs'
export async function GET(req: ApiRequest, res: ApiResponse) {
const { id } = req.params as { id: string }
const post = await db.getPost(id)
if (!post) {
res.json({ error: 'Post not found' }, 404)
return
}
res.json(post)
}
export async function POST(req: ApiRequest, res: ApiResponse) {
try {
const created = await db.createPost(req.body)
res.json(created, 201)
} catch (err) {
console.error('[POST /posts]', err)
res.json({ error: 'Internal server error' }, 500)
}
}Behaviour reference
| Scenario | Without error page | With error page |
|---|---|---|
| Server page throws | Plain-text Internal Server Error (500) | _500.tsx rendered with error props |
| Client component render error | React crashes the component subtree | _500.tsx rendered in-place, no reload |
| Unhandled event handler throw | Browser console error only | _500.tsx rendered in-place, no reload |
| Unhandled promise rejection | Browser console error only | _500.tsx rendered in-place, no reload |
| Unknown URL | Plain-text Page not found (404) | _404.tsx rendered with 404 status |
<Link> to unknown URL | Full page reload | In-place SPA navigation, no reload |
HMR save of _404.tsx / _500.tsx | — | Current page re-fetches immediately |
Both error pages are fully bundled into the production output for Node.js and Vercel — no runtime file-system access is required. The correct HTTP status code (404 or 500) is always set on the response, and errorStack is stripped in production so stack traces never reach end users.