🔵 App Router – Pages, Layouts & Navigation · Lesson 14
loading, error & not-found Pages
Three special files
- 1loading.jsx — shown while the page is fetching data
- 2error.jsx — shown if the page throws an error
- 3not-found.jsx — shown for unknown URLs or notFound()
loading.jsx — instant loading state
If a page waits for a slow database, the user sees a blank screen. Add loading.jsx next to the page and Next.js shows it immediately, then replaces it with the real page.
JSX – app/blog/page.jsx (slow)
// app/blog/page.jsx — simulate a slow database
async function getPosts() {
await new Promise((r) => setTimeout(r, 2000)) // 2 seconds
return [{ slug: 'hello', title: 'Hello Next.js' }]
}
export default async function Blog() {
const posts = await getPosts()
return posts.map((p) => <h2 key={p.slug}>{p.title}</h2>)
}JSX + CSS – app/blog/loading.jsx skeleton
// app/blog/loading.jsx
export default function Loading() {
return (
<div className="container" aria-busy="true">
{[1, 2, 3].map((i) => (
<div key={i} className="skeleton-card">
<div className="sk sk-title" />
<div className="sk sk-line" />
<div className="sk sk-line short" />
</div>
))}
</div>
)
}
/* globals.css */
.sk { background: linear-gradient(90deg,#eee 25%,#f5f5f5 50%,#eee 75%);
background-size: 200% 100%; animation: shine 1.2s infinite; border-radius: 8px; }
.sk-title { height: 22px; width: 60%; margin-bottom: 10px; }
.sk-line { height: 12px; margin-bottom: 8px; }
.sk-line.short { width: 40%; }
@keyframes shine { to { background-position: -200% 0; } }Suspense for part of a page
JSX – app/page.jsx
// app/page.jsx — hero appears instantly, reviews stream in later
import { Suspense } from 'react'
async function Reviews() {
const res = await fetch('https://api.example.com/reviews') // slow
const reviews = await res.json()
return <ul>{reviews.map((r) => <li key={r.id}>⭐ {r.text}</li>)}</ul>
}
export default function Home() {
return (
<main>
<h1>Learn coding with EduSite</h1> {/* instant */}
<Suspense fallback={<p>Loading reviews…</p>}>
<Reviews /> {/* streams later */}
</Suspense>
</main>
)
}error.jsx — catch crashes
JSX – app/error.jsx
// app/error.jsx (from the EduSite project)
'use client' // error boundaries must be Client Components
export default function Error({ error, reset }) {
return (
<div className="container section" style={{ textAlign: 'center' }}>
<h1>Something went wrong</h1>
<p className="lead">Please try again. If the problem continues, contact us.</p>
<button className="btn" onClick={() => reset()}>Try again</button>
</div>
)
}reset() tries to render the segment again — useful when a network call failed for a moment. Only the broken segment shows the error; the header and footer from the layout still work.
global-error.jsx
JSX – app/global-error.jsx
// app/global-error.jsx — catches errors in the ROOT layout itself
'use client'
export default function GlobalError({ reset }) {
return (
<html><body>
<h1>Site is temporarily unavailable</h1>
<button onClick={() => reset()}>Reload</button>
</body></html>
)
}not-found.jsx — real 404
JSX – app/not-found.jsx
// app/not-found.jsx (from the EduSite project)
import Link from 'next/link'
export default function NotFound() {
return (
<div className="container section" style={{ textAlign: 'center' }}>
<h1>404 – Page not found</h1>
<p className="lead">The page you are looking for does not exist or has moved.</p>
<Link href="/" className="btn">Go to home page</Link>
</div>
)
}
// Shown for unknown URLs AND whenever you call notFound()
How Next.js wraps your page
Component hierarchy (conceptual)
<Layout>
<ErrorBoundary fallback={<Error />}> ← error.jsx
<Suspense fallback={<Loading />}> ← loading.jsx
<ErrorBoundary fallback={<NotFound />}> ← not-found.jsx
<Page /> ← page.jsx
</ErrorBoundary>
</Suspense>
</ErrorBoundary>
</Layout>Common mistakes
- Forgetting
'use client'in error.jsx → build error. - Expecting error.jsx to catch errors in the same folder's layout → it catches the page and children; layout errors go to the parent's error.jsx or global-error.jsx.
- Showing a spinner for 50 ms data → unnecessary flicker; use loading UI where data is genuinely slow.
- Putting
loading.jsxabove a dynamic[slug]route → the response starts streaming with status 200, sonotFound()inside the page can no longer send a 404. For SEO pages, keep loading.jsx out of that path or setdynamicParams = false.
Summary
loading.jsx= automatic Suspense fallback;<Suspense>for one slow part.error.jsx(client) catches errors and offersreset().not-found.jsx+notFound()= real 404 pages.
✅ Practice
Add a 2-second delay to your blog page and build a 3-card skeleton loader. Then throw an error on purpose (throw new Error('test')) and see your error.jsx with a working "Try again" button.
Frequently Asked Questions
Why must error.jsx be a Client Component?
Error boundaries use React state on the browser to catch errors and offer a reset() retry, so the file must start with 'use client'.
Does loading.jsx hurt SEO?
No. Next.js streams the HTML; search engines receive the final content in the same response. The loading UI only helps users on slow connections.
What is the difference between loading.jsx and ?
loading.jsx wraps the whole page segment in Suspense automatically. A manual lets you show a fallback for only one slow part while the rest of the page appears immediately.
