Next.js Error Handling Guide: Managing API Errors with Axios and Fetch on Server & Client
Learn how to handle error in NextJs at production level in full details with best example . Either it's server or client easiest guide how to handle it from scratch using both Axios and Fetch . As well as error handling in Redux .
Topic for discussion :
- What are the differences in error handling between Axios and Fetch?
- How do we handle errors on the server-side in Next.js?
- How do we handle errors on the client-side in Next.js?
- How does RejectWithValue work in Redux for error handling?
- How do large organizations track and monitor errors in complex web applications?
Introduction
When building modern web applications with Next.js, ensuring a smooth user experience is just as important as delivering powerful features. One common challenge is handling errors that occur during API calls—whether they happen on the server-side while fetching data or on the client-side after rendering. Without proper handling, these errors can cause the UI to break, leaving users frustrated.
This is where Error Boundaries come in. They act as a safety net for your UI, catching unexpected errors and displaying a fallback interface instead of letting the entire page crash. By combining Error Boundaries with error handling in Axios and Fetch, we can create a robust and user-friendly interface that gracefully handles failures both on the server and client.
Note : Topic you see below is fully tested in NextJs and based on real implementation practices from our e-commerce project at Ewan Byte .
1. How to Handle API Errors in Next.js with Axios and Fetch
When making API calls in Nextjs , we can handle error using both Axios and Fetch which is crucial for maintaining a smooth UI. First we will create a reusable services hook for API call using both Fetch and Axios to know how it will be used in any components to catch the error . And we will see how to handled error using any one of the services , which is implemented in (question no .2 & no.3) . Below are the services explain briefly : -
1 .1 Using Fetch given below :

Note : If anything goes wrong while calling an API using your getData service — no matter which component calls it — the error will be caught inside your try.. catch, and you can show an alert, toast, or message right there in that component.
Explanation :
i. Fetch does not throw an error for HTTP errors like 404 or 500. It only throws for network errors.
ii. That’s why we manually check res.ok and throw a custom ApiError.
iii. Using ApiError (extending the native Error) allows us to include statusCode and other metadata, which is useful when displaying in Error Boundaries. ( which is described briefly on 1.3 )
iv . API_URL purpose is to create a clean, version-ready base API URL from environment variables which is created in config files with page name env.ts .
And in .env.local we have : NEXT_PUBLIC_API_URL=https://... ( your domain url ).

Example :
1 . If valid endpoint like : “/users” then it will load data successfully.
2 . If Invalid path like : “/produc” instead of “/product” then ApiError throws ( 404 ) .
3 . If wrong token then ApiError will throw (401/403) .
1.2 . Using Axios given below :

Explanation :
- Axios automatically throws an error for HTTP status codes outside the 2xx range.
- Axios’s error object contains response, status, and data, making it easier to extract the status code and message.
- Again, we wrap it in ApiError to standardize error handling, especially when connecting to a UI Error Boundary.
- instanceof checks whether an object was created from a specific class (or subclass) — in other words, whether it “belongs to” that class.
- Here , AxiosInstance creates and exports a preconfigured Axios instance with a base URL, timeout, and JSON headers — ready for API calls.

1.3 Why Use ApiError Extending Native Error ( custom error class ) ?

Explanation :
- As we have to do this because Native Error only provides message, stack , ….but no statusCode so inheriting(extends) the stausCode into the Native Error.
- We can attach statusCode, URLs, or other metadata to errors.
- This makes it easy to display meaningful messages in Error Boundaries instead of generic “Something went wrong.”
- Here , this.name will be helpful to check in catch block during api call in else if condition like : -

2 . Error Handling in Server-side ( Next.js )
In Next.js App Router, errors in server components are handled automatically at the route segment level with error.tsx.
Automatic Error Boundary → Each error.tsx acts like a React error boundary for that segment. If a server or client error occurs inside, it will "bubble up" to the nearest error.tsx.
404 Handling → Next.js provides a special not-found.tsx file. If you explicitly call notFound(), or if Next.js detects a missing resource, it renders this UI instead of throwing an error.
Runtime Errors → If an error is thrown (JSX rendering, API fetching, etc.), Next.js catches it and passes it to the nearest error.tsx file.
Custom API Errors → Since errors are just Error objects, and Error doesn’t natively carry statusCode, we need to serialize (JSON.stringify) error details (like statusCode, message) on the server before throwing. Later, in the error.tsx, we can JSON.parse it to show a meaningful fallback UI. This approach is often called error serialization .
Example : -
2.1 . With Serialization
Here we have use Fetch services and same implementation with Axios too : -

Note: As shown in the picture above, when throw new Error is triggered, it will be caught by error.tsx. The error will then be parsed and displayed on the client side after hydration.
#Implementaion on error.tsx : -
So , in the error.tsx we will be doing like this as shown in code snippets : -

Explanation :
- So , if backend sends proper JSON format like below : -
{ "message": "Product not found", "statusCode": 404 }
Then , based on above json it will parsed by try block and show its on UI .
- If , backend throws ( Non-JSON) format like this :
"Network error occurred"
Then , based on this it will not able to parsed by try block so catch block will execute in this case .
Conclusion with serialization : It's totally fine in development it will show fallback error and statusCode as you wanted .But in the production it will behave differently with statusCode 400 which is " Invalid data format " error. Like in case of missing UUID in any params it will show the error message . But in terms of other StatusCode it totally work fine both in development and production.
Note : But same issues in development it will show proper 400 and invalid data format in fallback UI .
But in server for same issues it will show such error as in picture below : -

Above image you see it's in the production because of missing UUID which I did intentionally in-order to check what error it will throw and I encounter such a long text message with generic 500 statusCode . If you ignore 400 status then for other it's fully production ready . If, you still want to handle it too below code without serialization is best for you .
Reason for the above issues we encounter : -
| Step | Local Dev | Production |
|---|---|---|
throw new Error(JSON.stringify(...)) |
Shows full stack trace, exposes the real error message, and allows the error to bubble to error.tsx | Swallows any error during server render, replaces it with a generic 500 page, and logs only a digest ID — to prevent leaking sensitive data |
return section |
Works | Works |
| Why? | Dev exposes errors for debugging | Prod hides errors for security |
2.2 . Without Serialization ( Best Production Ready Code )
Here, we return JSX instead of throwing an error. Throwing an error ( i.e throw error in catch block ) triggers the error.tsx which is the same process as with serialization, which can cause issues with 400 status codes in production.
By avoiding serialization (i.e., not using JSON.stringify), we also don’t need to use JSON.parse in error.tsx.

Error Handling with error.tsx
We still need to create an error.tsx file inside any app directory or route folder. This file will automatically catch runtime errors or uncaught exceptions at the route level — just like the default Next.js behavior.
Below is an example of an error.tsx setup for this approach:

Note:
The error.tsx file can be made reusable by allowing it to accept error as a prop. This makes it easy to use across multiple routes.
The Fallback component is a custom UI built to maintain consistent error styling and messaging across both server and client sides.
3 . Error Handling in Client-side ( Next.js )
Client-side error handling is a bit manual compared to server-side:
No error.tsx usage → Unlike the server, error.tsx only works in server components. In client components, you need to manage errors in state.
Runtime errors → Client-side runtime errors (like undefined, null, bad .map) do bubble up to error.tsx, since that’s a React error boundary, but API call errors won’t.
404 Handling → Unlike server-side where notFound() works automatically, in client components you must handle it yourself. This is usually done with router.push("/not-found") if API returns a 404.
Fallback UI → Typically, you manage an error state (setError) and display a fallback message or component to the user.
Example : -

Here , If you want to show the Status-Code then set it in state as error-message.
So , based on above error state store we can show fallback UI to user as below : -

The remaining questions will be published as soon as the writing on those topics is finished.
Comments ()