Skip to main content

Handling errors

@zimic/fetch fully types the requests and responses based on your schema. If a response fails with a status code in the 4XX or 5XX ranges, the response.ok property will be false. In this case, response.error will contain a FetchResponseError representing the failure.

Handling response errors​

To handle errors, check the response.status or the response.ok properties to determine if the request was successful or not. Alternatively, you can throw the response.error to handle it upper in the call stack.

The onResponse listener can be a good strategy if you want to handle errors transparently. The listener can automatically retry the request without bubbling the error up to the caller. This is useful for recoverable errors, such as expired tokens, network errors, or temporarily unavailable services. You can also use it to log errors.

As an example, consider the following schema:

import { HttpSchema } from '@zimic/http';

interface User {
id: string;
username: string;
}

type Schema = HttpSchema<{
'/users/:userId': {
GET: {
request: {
headers?: { authorization?: string };
};
response: {
200: { body: User };
401: { body: { code: 'UNAUTHORIZED'; message: string } };
403: { body: { code: 'FORBIDDEN'; message: string } };
404: { body: { code: 'NOT_FOUND'; message: string } };
500: { body: { code: 'INTERNAL_SERVER_ERROR'; message: string } };
503: { body: { code: 'SERVICE_UNAVAILABLE'; message: string } };
};
};
};
}>;

A GET request to /users/:userId may be successful with a 200 status code, or it may fail with a 401, 403, 404, 500, or 503. After receiving the response, we can check the status code and handle errors accordingly.

import { createFetch } from '@zimic/fetch';

const fetch = createFetch<Schema>({
baseURL: 'http://localhost:3000',
});

async function fetchUser(userId: string) {
const response = await fetch(`/users/${userId}`, {
method: 'GET',
});

// If the user was not found, return null
if (response.status === 404) {
return null;
}

// If the request failed with other errors, throw them
if (!response.ok) {
throw response.error;
}

// At this point, we know the request was successful
const user = await response.json(); // User
return user;
}
TIP: Throwing unknown errors

Checking the response.ok and response.status properties is a good practice handle errors. A common strategy is to first check status codes that require specific logic, depending on your application, and throwing other errors a global error handler.

if (response.status === 401) {
redirectToLogin();
}

if (response.status === 404) {
return null;
}

if (response.status === 500) {
showMessage('An unexpected error occurred.');
}

if (response.status === 503) {
showMessage('The service is temporarily unavailable.');
}

// Throw any other error
if (!response.ok) {
throw response.error;
}

Logging response errors​

fetchResponseError.toObject() is useful get a plain object representation of the error. This makes the error easier to log and inspect.

import { FetchResponseError } from '@zimic/fetch';

if (error instanceof FetchResponseError) {
const plainError = error.toObject();
console.error(plainError);
}

You can also use JSON.stringify() or a logging library such as pino to serialize the error.

import { FetchResponseError } from '@zimic/fetch';

if (error instanceof FetchResponseError) {
const plainError = error.toObject();
console.error(JSON.stringify(plainError));
}

Request and response bodies are not included by default in the result of toObject. If you want to see them, use includeRequestBody and includeResponseBody. Note that the result will be a Promise that needs to be awaited.

import { FetchResponseError } from '@zimic/fetch';

if (error instanceof FetchResponseError) {
const plainError = await error.toObject({
includeRequestBody: true,
includeResponseBody: true,
});
console.error(JSON.stringify(plainError));
}

If you are working with form data or blob bodies, such as file uploads or downloads, logging the body may not be useful as binary data won't be human-readable and may be too large. In that case, you can check the request and response and include the bodies conditionally.

import { FetchResponseError } from '@zimic/fetch';

if (error instanceof FetchResponseError) {
const requestContentType = error.request.headers.get('content-type');
const responseContentType = error.response.headers.get('content-type');

const plainError = await error.toObject({
// Include the body only if the content type is JSON
includeRequestBody: requestContentType === 'application/json',
includeResponseBody: responseContentType === 'application/json',
});
console.error(JSON.stringify(plainError));
}