Skip to main content

Error Handling

Next.js error handling differs from React due to its layered architecture. Understanding where errors originate and how they're transformed is key to effective error handling.

Architecture Overview

Client Component ──► Generated Route ──► Server Function ──► External API

(normalizes errors)

Server Component ──────────────────────► Server Function ──► External API

(throws on error)
  • Client-side: Hooks call generated API routes, which call server functions, which call external APIs. Errors are normalized by the route layer before reaching the client.
  • Server-side: Server functions can be called directly from Server Components or during SSR. Errors surface as thrown exceptions.

Client-Side Error Handling

Client-side hooks receive the same error types as the React SDK. When isError(state) is true, state.error is an IntrigError.

Error Types

All errors surface as HttpError on the client because the route layer transforms exceptions into HTTP responses:

Original ErrorRoute ResponseClient Receives
AxiosError (4xx/5xx)JSON with original statusHttpError with API's status and body
ZodError (validation)400 with { errors: [...] }HttpError with status 400
Unknown exception500 with error detailsHttpError with status 500

Type Guards

import { isError, isHttpError } from '@intrig/next';

const [state, execute, clear] = useGetEmployee();

if (isError(state) && isHttpError(state.error)) {
const { status, body } = state.error;

if (status === 400 && body?.errors) {
// Validation error from route layer
const fieldErrors = body.errors as Array<{ path: string; message: string }>;
} else if (status === 401) {
// Unauthorized
} else if (status >= 500) {
// Server error
}
}

Validation Error Shape

When the route layer catches a ZodError, it returns:

{
errors: Array<{
path: string; // Dot-notation field path (e.g., "user.email")
message: string; // Validation message
}>
}

Request Validation

Like React, the execute function returns a DispatchState for immediate validation feedback:

import { isValidationError, isSuccessfulDispatch } from '@intrig/next';

const result = createEmployee(data, params);

if (isValidationError(result)) {
// Request not sent — validation failed client-side
console.log(result.error);
}

Error Display Component

import { isError, isHttpError, NetworkState } from '@intrig/next';

function ApiError({ state }: { state: NetworkState<any> }) {
if (!isError(state)) return null;

const { error } = state;
if (!isHttpError(error)) return null;

const { status, body } = error;

// Route-layer validation error
if (status === 400 && Array.isArray(body?.errors)) {
return (
<ul>
{body.errors.map((err: { path: string; message: string }, i: number) => (
<li key={i}>{err.path}: {err.message}</li>
))}
</ul>
);
}

// API error with message
if (body?.message) {
return <p>{body.message}</p>;
}

// Generic HTTP error
return <p>Request failed with status {status}</p>;
}

Server-Side Error Handling

Server functions throw exceptions on failure. Use standard try/catch patterns.

Error Types

Server functions throw raw AxiosError from the underlying HTTP client:

import { AxiosError, isAxiosError } from 'axios';

try {
const { data } = await getEmployee({ id });
} catch (e) {
if (isAxiosError(e)) {
const status = e.response?.status;
const body = e.response?.data;
// Handle based on status/body
}
}

Server Component Example

// app/employees/[id]/page.tsx
import { getEmployee } from '@intrig/next/employee_api/employees/getEmployee';
import { isAxiosError } from 'axios';
import { notFound } from 'next/navigation';

export default async function EmployeePage({ params }: { params: { id: string } }) {
try {
const { data: employee } = await getEmployee({ id: params.id });
return <EmployeeDetails employee={employee} />;
} catch (e) {
if (isAxiosError(e) && e.response?.status === 404) {
notFound();
}
throw e; // Let error boundary handle
}
}

Server Action Example

'use server';

import { createEmployee } from '@intrig/next/employee_api/employees/createEmployee';
import { isAxiosError } from 'axios';

export async function createEmployeeAction(formData: FormData) {
try {
const { data } = await createEmployee({
name: formData.get('name') as string,
email: formData.get('email') as string,
});
return { success: true, data };
} catch (e) {
if (isAxiosError(e)) {
return {
success: false,
status: e.response?.status,
error: e.response?.data
};
}
return { success: false, error: 'Unknown error' };
}
}

Error Boundary Integration

For unhandled errors in Server Components, use Next.js error boundaries:

// app/employees/error.tsx
'use client';

export default function EmployeeError({
error,
reset,
}: {
error: Error & { digest?: string };
reset: () => void;
}) {
return (
<div>
<h2>Failed to load employee</h2>
<button onClick={reset}>Retry</button>
</div>
);
}

Client vs Server Comparison

AspectClient-SideServer-Side
Error sourcestate.error (after route normalization)Thrown exception
Primary typeHttpErrorAxiosError
Validation errorsHttpError with 400 statusThrown before request
Type guardsisHttpError(), etc.isAxiosError()
Recoveryclear() and re-executeRetry in catch block

Imports Summary

// Client-side (same as React)
import {
isError,
isHttpError,
isValidationError,
isSuccessfulDispatch
} from '@intrig/next';

// Server-side
import { isAxiosError } from 'axios';