Handling 404s and Errors in Next.js
Cover photo by David Pupăză on Unsplash
Do you need a complex global error handling strategy in App Router? No, you really don’t.
Keep it simple. Use the built-in error.js and not-found.js file conventions provided by Next.js. I see too many developers wrapping their entire application layout in massive, custom error boundaries that just end up catching everything and hiding the real problems. It’s lazy and makes debugging a nightmare.
Why the default approach works
The framework handles the isolation of errors automatically. By placing an error.js file in a specific segment, you scope the boundary to that part of the UI. If a sidebar breaks, only the sidebar shows the error state while the rest of the page keeps working. It’s clean. It’s predictable.
'use client';
export default function Error({ error, reset }) { return ( <div> <h2>Something went wrong!</h2> <button onClick={() => reset()}>Try again</button> </div> );}What about 404s?
Don’t reinvent the wheel. Just use notFound() from next/navigation in your Server Components. It triggers the not-found.js file in the nearest directory. (I’ll admit, this can get annoying if you want a custom 404 message based on the specific resource that wasn’t found.)
“But Namir, shouldn’t I log these to an external service?” Yes, you should. Use the error component to send logs to your monitoring tool of choice, but keep the UI component itself focused on a graceful recovery. Don’t build a massive state machine for something the browser or the framework should manage.
The caveat
I should mention that error.js only catches errors in Client Components or during the rendering of the boundary itself. If your database call in a Server Component throws, it bubbles up until it hits the root layout. If you don’t have a root error.js or global-error.js, your user just gets a blank white screen. Always define a root error boundary. It saves you from having to explain a blank page to your boss on a Monday morning.
Don’t over-complicate your boundaries. If you’re building a massive custom provider to track every single error across your app, you’re likely creating more technical debt than you’re solving. Stick to the conventions and keep your UI lean. Are you doing anything more complex than this in your own projects?