Error handling forms the backbone of production-ready Next.js applications. When users encounter errors, the difference between a frustrating experience and a smooth one often comes down to how gracefully your application manages those failures.
Without thoughtful error strategies, Next.js apps risk crashing unexpectedly, leaking sensitive stack traces, or presenting users with cryptic technical messages.
This guide explores error-handling approaches explicitly tailored for Next.js applications.
What are errors in Next.js?
Next.js applications face unique error challenges due to their hybrid rendering model and framework-specific behaviors. Before implementing solutions, let's categorize the distinct error types you'll encounter.
Runtime failures during normal operation
These predictable errors occur during typical application usage and should be anticipated in your error handling strategy. They represent edge cases rather than code defects.
Consider these common examples in Next.js applications:
- Users navigate to non-existent pages, triggering 404 scenarios
- Server-side data fetching times out when external APIs are unresponsive
- Form validation rejects improper user inputs before submission
- Authentication tokens expire during a user session
- Environment-specific issues occur between development and production
Developer mistakes and code Ddefects
These represent actual bugs in your application logic that require fixes rather than runtime handling. In Next.js, these often have framework-specific manifestations.
For example, incorrectly implementing React hooks within components can trigger the "Rules of Hooks" error during rendering. Incorrect prop types passed between components lead to type errors that break component rendering chains.
Forgetting to handle promise rejections in data fetching functions causes unhandled exceptions during server-side rendering. Misusing Next.js data fetching methods like getStaticProps
or getServerSideProps
results in build-time or runtime failures.
Next.js framework violations
The Next.js framework imposes specific constraints that, when violated, generate distinctive errors requiring specialized handling approaches.
Client-side navigation can fail when routing configurations contain errors or circular dependencies. Hydration mismatches occur when server-rendered HTML doesn't align with client-side React component structure, causing visible content "flickers" and console warnings.
Data fetching methods like getStaticProps
failing during build time prevents successful deployment. Static Generation paths misconfigured can lead to build failures or missing dynamic routes.
With these error categories in mind, let's explore implementation strategies for each part of the Next.js application architecture.
Pages router error handling
The Pages Router in Next.js provides a hierarchy of error handling mechanisms that operate at different levels of granularity. Understanding when to use each approach is key to building resilient applications.
Component error boundaries
Error Boundaries represent React's primary defense against runtime errors in component trees. They function as JavaScript catch blocks for your components, preventing errors from bubbling up and crashing your entire application. In Next.js, they're significant because rendering errors can occur during hydration when the server-rendered DOM doesn't match the client-side component structure.
Error Boundaries catch exceptions during rendering, in lifecycle methods, and constructors of their child component hierarchies. However, they have significant limitations in the Next.js context - they don't catch errors in event handlers, asynchronous code (like setTimeout
or fetch
), or server-side rendering. They also don't catch errors in the boundary component itself.
Here's an example:
import { Component } from 'react';
class ErrorBoundary extends Component {
constructor(props) {
super(props);
this.state = { hasError: false };
}
static getDerivedStateFromError() {
return { hasError: true };
}
componentDidCatch(error, errorInfo) {
console.error('Error caught:', error, errorInfo);
}
render() {
if (this.state.hasError) {
return this.props.fallback || <h2>Something went wrong</h2>;
}
return this.props.children;
}
}
export default ErrorBoundary;
The strategic placement of Error Boundaries in your component hierarchy is crucial for effective error isolation. Apply them around critical components to prevent cascading failures:
import ErrorBoundary from '@/components/ErrorBoundary';
import ProductDetails from '@/components/ProductDetails';
import RelatedProducts from '@/components/RelatedProducts';
import ProductReviews from '@/components/ProductReviews';
export default function ProductPage({ product }) {
return (
<div className="product-page">
<h1>Product Details</h1>
{/* Isolate the main product display errors */}
<ErrorBoundary fallback={<div>Failed to display product</div>}>
<ProductDetails product={product} />
</ErrorBoundary>
{/* Separate boundary for related products section */}
<ErrorBoundary fallback={<div>Unable to load related products</div>}>
<RelatedProducts productId={product.id} />
</ErrorBoundary>
{/* Another boundary for reviews section */}
<ErrorBoundary fallback={<div>Reviews temporarily unavailable</div>}>
<ProductReviews productId={product.id} />
</ErrorBoundary>
</div>
);
}
This compartmentalized approach prevents a rendering error in one section (like reviews) from breaking the entire product page. Users can still view the product details and related items even if the reviews component crashes. This pattern is particularly valuable in Next.js applications where different components might load and process data differently.
For components that are reused across multiple pages, consider wrapping the ErrorBoundary directly in the component definition to ensure consistent error handling everywhere the component is used.
Custom error pages
Next.js provides built-in support for application-wide error handling through custom error pages. These override the framework's default error pages with tailored experiences that maintain your site's branding and provide better recovery options for users.
Unlike component-level Error Boundaries, these custom pages handle routing-level errors outside your component tree, such as when a page doesn't exist or when server-side rendering fails completely.
For 404 errors, create a pages/404.js
file to intercept navigation to non-existent routes:
import Link from 'next/link';
export default function Custom404() {
return (
<div className="error-container">
<h1>Page Not Found</h1>
<p>We couldn't find what you were looking for.</p>
<Link href="/">Return Home</Link>
</div>
);
}
The 404 page is triggered automatically when users navigate to paths that don't match any defined routes in your application. It's also activated when you return { notFound: true }
from getStaticProps
or getServerSideProps
.
For server-side errors, create a pages/500.js
file:
import Link from 'next/link';
import { useEffect } from 'react';
export default function Custom500() {
useEffect(() => {
// Report server error to analytics
// This helps track which pages are generating server errors
const reportError = async () => {
await fetch('/api/error-tracking', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
type: '500_error',
path: window.location.pathname,
referrer: document.referrer
})
}).catch(console.error);
};
reportError();
}, []);
return (
<div className="error-container">
<h1>Server Error</h1>
<p>We're sorry, something went wrong on our end.</p>
<p>Our team has been notified and is working on a fix.</p>
<div className="error-actions">
<button onClick={() => window.location.reload()}>
Try Again
</button>
<Link href="/">Return Home</Link>
</div>
</div>
);
}
The 500
page is displayed when server-side rendering encounters an unhandled exception. This includes errors in getStaticProps
, getServerSideProps
, or during the initial page render on the server. Unlike client-side Error Boundaries, the 500 page is a complete page replacement rather than an in-place component fallback.
These custom error pages should be designed to match your site's design language while providing clear recovery paths. They serve as the last line of defense for errors that escape your more granular error handling mechanisms.
API route error handling
Next.js API routes function as your application's backend endpoints, making error handling essential for maintaining a stable API contract. Unlike page components, errors in API routes must be explicitly transformed into proper HTTP responses:
export default async function handler(req, res) {
try {
// Method validation
if (req.method !== 'GET') {
return res.status(405).json({
success: false,
error: 'Method not allowed'
});
}
const { id } = req.query;
// Input validation
if (!id || !/^\d+$/.test(id)) {
return res.status(400).json({
success: false,
error: 'Invalid product ID format'
});
}
try {
const product = await fetchProduct(id);
if (!product) {
return res.status(404).json({
success: false,
error: 'Product not found'
});
}
return res.status(200).json({
success: true,
data: product
});
} catch (fetchError) {
// Handle external service errors
if (fetchError.code === 'ECONNREFUSED') {
return res.status(503).json({
success: false,
error: 'Service temporarily unavailable'
});
}
throw fetchError;
}
} catch (error) {
console.error('API error:', error);
return res.status(500).json({
success: false,
error: 'Internal server error',
requestId: `req_${Date.now().toString(36)}`
});
}
}
This pattern ensures consistent response structures that follow a standard format with success indicators and appropriate error details.
Error responses include context-specific information that helps clients understand issues without exposing sensitive system details. Early input validation prevents unnecessary database or external API calls when request data is malformed.
The nested try/catch approach enables specialized handling for external service failures versus general application errors. Including a request identifier in error responses helps correlate client-reported problems with server logs for more efficient troubleshooting.
Through this structured approach to API errors, you create interfaces that gracefully handle failures while providing meaningful feedback to clients.
App router error handling
The App Router introduced in Next.js 13 transforms error handling through a file-based convention system. This approach aligns error boundaries with route structures, creating an intuitive and maintainable error handling hierarchy.
Route segment error components
The App Router's error handling centers around special error.js
files that function as boundaries for specific route segments. These files create isolation zones that prevent errors from affecting other parts of your application.
'use client';
import { useEffect } from 'react';
export default function DashboardError({ error, reset }) {
useEffect(() => {
// Report error to analytics
reportError('dashboard_error', {
message: error.message,
stack: error.stack
});
}, [error]);
return (
<div className="error-container">
<h2>Dashboard Error</h2>
<p>{error.message || 'An unexpected error occurred'}</p>
<div className="error-actions">
<button onClick={() => reset()}>
Try Again
</button>
<a href="/support">Contact Support</a>
</div>
{process.env.NODE_ENV === 'development' && (
<pre>{error.stack}</pre>
)}
</div>
);
}
This error component automatically reports errors to analytics when they occur. It distinguishes between different error types and provides a recovery mechanism through the reset
function. Technical details appear only in development, and users have multiple recovery options. When errors happen in the dashboard route or its children, this boundary captures them and preserves the application shell, including navigation and headers.
The reset
function enables retry logic by re-rendering the route segment - particularly valuable for transient errors like network timeouts.
Global error handling
For catastrophic root-level errors, the App Router provides a special global-error.js
file:
'use client';
import { useEffect } from 'react';
export default function GlobalError({ error, reset }) {
useEffect(() => {
// Report critical error
captureException(error, { level: 'fatal' });
}, [error]);
return (
<html>
<head>
<title>Critical Error</title>
<meta name="viewport" content="width=device-width, initial-scale=1" />
</head>
<body>
<div className="global-error-container">
<h1>We're Sorry</h1>
<p>We've encountered a critical error.</p>
<div className="error-actions">
<button onClick={() => reset()}>Try Again</button>
<button onClick={() => window.location.href = '/'}>
Return Home
</button>
</div>
</div>
</body>
</html>
);
}
The global error component must return a complete HTML document as it replaces the entire application. Global errors represent catastrophic failures that prevented the application shell from rendering and should be treated highly. This page provides a stripped-down experience focused solely on communicating the error with basic recovery options. Since application state is unavailable, providing a way to return to the home page is essential.
Global error rendering occurs only when errors happen in the root layout or template, making it relatively rare in well-designed applications.
Not-found handling
The App Router introduces a specialized not-found.js
file for missing content scenarios:
import Link from 'next/link';
import { SearchBox } from '@/components/SearchBox';
import { RecentPosts } from '@/components/RecentPosts';
export default function BlogPostNotFound() {
return (
<div className="content-not-found">
<h2>Blog Post Not Found</h2>
<p>The post you're looking for doesn't exist or was moved.</p>
<div className="recovery-options">
<SearchBox placeholder="Search blog posts..." />
<RecentPosts count={3} />
<div className="navigation-links">
<Link href="/blog">All Blog Posts</Link>
<Link href="/blog/categories">Browse Categories</Link>
</div>
</div>
</div>
);
}
Trigger this component programmatically when content retrieval fails:
import { notFound } from 'next/navigation';
export default async function BlogPost({ params }) {
const post = await getBlogPost(params.slug);
if (!post) {
notFound(); // Activates the not-found.js component
}
return <BlogPostLayout post={post} />;
}
The not-found component receives full route context, displaying relevant alternative content. Next.js automatically returns the correct 404 status code for search engines while rendering your custom UI. You can offer content suggestions, search functionality, and browsing options for the current section. Unlike error boundaries that replace content, the not-found component preserves shared layout and navigation structure.
This approach works especially well for content-heavy applications where missing items are expected rather than exceptional.
Server component error handling
Server Components in Next.js introduce unique error handling challenges because they execute on the server rather than in the browser. Their server-first rendering model means traditional React error boundaries cannot catch their errors directly, requiring alternative approaches to manage failures gracefully.
try/catch
patterns
Server Components support straightforward error handling using JavaScript's native try/catch blocks. This approach provides immediate error containment without additional components or complex patterns:
export default async function ProductsPage() {
try {
const products = await fetchProducts();
if (products.length === 0) {
return <div>No products available</div>;
}
return <ProductList products={products} />;
} catch (error) {
console.error('Product fetch error:', error);
return <div>Failed to load products</div>;
}
}
This pattern elegantly handles data fetching errors by returning alternate UI directly from the Server Component. Since Server Components can return JSX, you can provide contextual fallback experiences based on different error conditions. This approach keeps error handling close to the data fetching logic, making the code more maintainable and easier to reason about.
The direct try/catch
method works well for simple error states, but offers limited interactivity since Server Components cannot use hooks or respond to client-side events. This approach provides excellent performance with minimal complexity for static error messages and basic fallbacks.
Server/client component combination
For richer, interactive error experiences, Next.js enables a powerful pattern: using Server Components for data fetching and Client Components for error handling. This approach combines server efficiency with client interactivity:
export default function ErrorableProductList({ products, error }) {
if (error) {
return (
<div className="error-state">
<p>Error: {error.message}</p>
<button onClick={() => window.location.reload()}>
Retry
</button>
</div>
);
}
return <div>{/* Product rendering */}</div>;
}
Then in your Server Component:
import ErrorableProductList from '@/components/ErrorableProductList';
export default async function ProductsPage() {
try {
const products = await fetchProducts();
return <ErrorableProductList products={products} />;
} catch (error) {
return <ErrorableProductList error={error} />;
}
}
This hybrid pattern offers advantages by enabling interactive error states with retry mechanisms, animations, and user-triggered recovery flows. The Server Component handles data fetching and initial error capture, then delegates the UI rendering to a Client Component that can manage state and user interactions.
The pattern is particularly valuable for complex data-dependent features where users benefit from retry capabilities, error details, or alternative navigation options. The Client Component gains rich context about the failure by passing the error as a prop while maintaining a clear separation between data fetching and UI rendering concerns.
This separation also improves performance, as the error handling logic hydrates in the browser only when needed, preserving the performance benefits of Server Components when operations succeed.
Final thoughts
Good error handling isn't just about preventing crashes—designing recovery paths that preserve user trust.
When properly implemented, these techniques ensure users receive meaningful feedback instead of cryptic errors, with clear paths to continue their journey despite encountered issues.
The result is a resilient application that maintains composure even under unexpected conditions, delivering a professional experience that builds confidence in your product.
Make your mark
Join the writer's program
Are you a developer and love writing and sharing your knowledge with the world? Join our guest writing program and get paid for writing amazing technical guides. We'll get them to the right readers that will appreciate them.
Write for us
Build on top of Better Stack
Write a script, app or project on top of Better Stack and share it with the world. Make a public repository and share it with us at our email.
community@betterstack.comor submit a pull request and help us build better products for everyone.
See the full list of amazing projects on github