Error Handling

Learn more about how to handle errors in CallApi

It's prevalent knowledge that making network requests is inherently risky. Things can go wrong for many reasons:

  • The server might be down.
  • The server might respond with an error status like 404 Not Found (the resource doesn't exist) or 500 Internal Server Error (something broke on the server).
  • There might be a network issue.
  • The request might timeout.
  • The response data might not be in the format you expected (e.g., not valid JSON).

When using the standard browser fetch API, handling these failures can sometimes be a bit clunky due to the following reasons:

  • Network errors throw one type of error
  • Non-2xx HTTP responses don't throw errors by default (you have to check response.ok)
  • Parsing errors might throw yet another type

This can lead to complex if/else chains and unwieldy try...catch blocks just to figure out what went wrong.

CallApi aims to make dealing with these failures much more predictable and convenient.

Structure of the error property

As introduced in the Getting Started guide, CallApi wraps responses in a result object with three key properties: data, error, and response.

When something goes wrong while making a request, the error property will contain a structured object detailing the problem.

The error property is an object that has the following properties:

  1. name: A string identifying the type of error (e.g., 'HTTPError', 'ValidationError', 'TypeError', 'TimeoutError', ...etc).

  2. message: A brief description of what went wrong:

    • For HTTP errors: The error message from the server, or if not provided, falls back to the defaultHTTPErrorMessage option
    • For validation errors: A formatted error message derived from the validation issues array
    • For non-HTTP errors: The error message from the JavaScript error object that caused the error
  3. errorData: The detailed error information:

    • For HTTP errors: It is set to the parsed error response from the API
    • For validation errors: It is set to the validation issues array
    • For non-HTTP errors: It is set to false
  4. originalError: The original error object that caused the error:

    • For HTTP errors: HTTPError
    • For validation errors: ValidationError
    • For non-HTTP errors: The underlying javascript error object (e.g., TypeError, DOMException, etc.)
api.ts
import {  } from "@zayne-labs/callapi";

const {  } = await ("https://my-api.com/api/v1/session");
Hover over the error object to see the type

Handling HTTP Errors (HTTPError)

One of the most common types of errors you'll encounter is when the server responds with a status code outside the 200-299 range (like 400, 401, 403, 404, 500, 503, etc.). Standard fetch doesn't throw an error for these responses.

CallApi, by default, wraps these responses in an HTTPError. You can customize the error response data type by providing a second generic type argument to callApi.

api.ts
import {  } from "@zayne-labs/callapi";

type  = {
	: <string | string[]>;
	: string;
};

const {  } = await <unknown, >("/api/endpoint");

if () {
	.(.);
}

Since the error property is a discriminated union, you can use the isHTTPError utility from @zayne-labs/callapi/utils to check if it's an HTTP error:

api.ts
import {  } from "@zayne-labs/callapi";
import {  } from "@zayne-labs/callapi/utils";

type  = {
	: boolean;
	: number;
	: string;
	: string;
};

type  = {
	?: <string | string[]>;
	?: string;
};

const { ,  } = await <, >("https://my-api.com/api/v1/session");

if (()) {
	.();
	.(.); // 'HTTPError'
	.(.);
	.(.); // Will be set to the error response data
}

Handling Validation Errors (ValidationError)

While covered in more detail in the Validation section, CallApi also has built-in support for schema validation. If you configure a schema (whether for any request option or for the response data) and the received data does not match that schema, CallApi will wrap this failure in a ValidationError.

You can use the isValidationError utility to check specifically for this error type:

api.ts
import {  } from "@zayne-labs/callapi";
import {  } from "@zayne-labs/callapi/utils";
import {  } from "zod";

const  = .({
	: .(),
	: .(),
	: .(),
	: .(),
});

const { ,  } = await ("https://my-api.com/api/v1/session", {
	: {
		: ,
	},
});

if (()) {
	.();
	.(.); // 'ValidationError'
	.(.);
	.(.); // Will be set to the validation issues array
}

As seen in the case above, if the data received from the API does not match the schema, the ValidationError thrown will be captured in the error property.

Handling Thrown Errors

Sometimes you might prefer that errors are thrown instead of returned in the result object. Set throwOnError to true to enable this behavior. This is useful when integrating with libraries that expect promises to reject on failure (like TanStack Query).

api.ts
import {  } from "@zayne-labs/callapi";
import { ,  } from "@zayne-labs/callapi/utils";

type  = {
	: boolean;
	: number;
	: string;
	: string;
};

type  = {
	?: <string | string[]>;
	?: string;
};

try {
	const {  } = await <>("https://my-api.com/api/v1/session", {
		: true,
	});
} catch () {
	if (<>()) {
		.();
		.(.);
		.(.);
		.(.);
	}

	if (()) {
		.();
		.(.);
		.(.);
		.(.);
	}
}

Conditional throwing:

You can also pass a function to throwOnError for conditional throwing based on the error context:

api.ts
import {  } from "@zayne-labs/callapi";
import { ,  } from "@zayne-labs/callapi/utils";

// Only throw for authentication errors
const  = await ("https://my-api.com/api/v1/session", {
	: ({  }) => ?. === 401,
});

// Throw for client errors (user mistakes) but not server errors (temporary issues)
const  = await ("https://my-api.com/api/users", {
	: ({  }) => {
		if (!) {
			return false;
		}

		return . >= 400 && . < 500;
	},
});

// Complex conditional logic based on error type and context
const  = await ("https://my-api.com/api/sensitive", {
	: ({ , ,  }) => {
		// Always throw validation errors - data integrity is critical
		if (()) {
			return true;
		}

		// Throw HTTP errors for sensitive endpoints
		if (() && ?. === 403 && .?.("/sensitive")) {
			return true;
		}

		// Throw rate limiting errors during business hours (handle differently off-hours)
		if (?. === 429) {
			const  = new ().();
			return  >= 9 &&  <= 17;
		}

		// Return other errors in result object
		return false;
	},
});

The data and error properties as a discriminated union

Another way to look at the data and error properties is as a pair of mutually exclusive properties.

This implies that:

  • If error is present, data is null.
  • If error is null, data is present.

TypeScript understands this relationship. So if you check for error first and handle it, TypeScript can automatically narrow the type of data to exclude null.

api.ts
import {  } from "@zayne-labs/callapi";
import {  } from "@zayne-labs/callapi/utils";

type  = {
	: boolean;
	: number;
	: string;
	: string;
};

type  = {
	?: <string | string[]>;
	?: string;
};

const { ,  } = await <, >("https://my-api.com/api/v1/session");

if (()) {
	.();
} else if () {
	.();
} else {
	.(); // TypeScript knows data is not null here
}
Hover over the data object to see the narrowed type
Edit on GitHub

Last updated on

On this page