Errors¶
Error classes, error-body parsers and error-event types used by the HTTP client. Everything the HTTP client throws is a UserError (or its subclass ApiResponseError), so you only ever need one catch shape.
import {
UserError,
ApiResponseError,
defaultErrorBodyParser,
makeResponseError,
makeNetworkError,
} from '@msflib/typescript';
import type {
ErrorBodyParser,
ParsedApiError,
ClientErrorDispatch,
ApiErrorDetail,
NetworkErrorDetail,
} from '@msflib/typescript';
Table of Contents¶
- What gets thrown, when
- UserError
- ApiResponseError
- Supported error body shapes
- Custom error body parser
- Error events (emitError)
- Low-level helpers
- Types
What gets thrown, when¶
| Situation | Thrown | message |
|---|---|---|
| Non-2xx response, status < 500 | ApiResponseError |
Parsed from the body (see shapes), or "Request failed" |
| Non-2xx response, status >= 500 | ApiResponseError |
Always "Could not process request. Please contact support." - real message on error.cause.message |
fetch itself rejects (offline, DNS, CORS...) |
UserError (not ApiResponseError) |
"Network error. Try again later." - original error on error.cause |
2xx response whose body is not JSON (e.g. 204 No Content) |
Raw SyntaxError from response.json() (not a UserError) |
Runtime-specific |
Empty / non-JSON success responses
Every client method calls response.json() on success. An endpoint that returns 204 No Content (common for DELETE) will reject with a plain SyntaxError, not a UserError. Wrap such calls yourself, or have the endpoint return a JSON body.
UserError¶
Base error class. Its message is always intended to be safe to show to an end user.
class UserError extends Error {
name: 'UserError';
cause?: unknown;
constructor(message: string, cause?: unknown);
static fromError(error: Error, message?: string): UserError;
}
UserError.fromError(err, msg?) wraps an existing error, keeping it on cause and using msg (or err.message) as the message.
ApiResponseError¶
Thrown for every non-2xx response. Extends UserError, so instanceof UserError checks keep working.
| Property | Type | Notes |
|---|---|---|
status |
number |
HTTP status code |
statusText |
string |
HTTP status text |
message |
string |
Parsed message, or the generic support message for 5xx |
errors |
Record<string, string[]> (optional) |
Per-field validation errors, when the body shape provides them - for any status code |
raw |
unknown |
The parsed JSON body. undefined if the body was not valid JSON |
cause |
unknown |
For 5xx: an Error whose message is the real parsed server message. Otherwise undefined |
import { createApiClient, ApiResponseError } from '@msflib/typescript';
const { apiClient } = createApiClient({
baseURL: 'https://api.example.com',
});
try {
await apiClient('POST', '/signup', payload);
} catch (error) {
if (error instanceof ApiResponseError) {
console.log(error.status); // 422
console.log(error.message); // "Email already taken."
console.log(error.errors); // { email: ["Email already taken."] }
}
}
For 5xx responses, error.message is swapped for a generic "Could not process request. Please contact support." (so it's always safe to show a user directly), while the real server message is kept on error.cause.message. error.status, error.raw, and error.errors are preserved either way.
If the error body is not JSON (e.g. an HTML 502 page from a proxy), raw is undefined, the parsed message falls back to "Request failed", a warning is logged with console.warn, and no emitError event fires.
Supported error body shapes¶
error.message/error.errors are derived by defaultErrorBodyParser, which tries each of these in order and uses the first that matches:
- FastAPI:
{ detail: string }or{ detail: [{ msg, loc, type }] }. For the array form,messageis the first item'smsg, anderrorsis keyed by the last element of each item'sloc(e.g.["body", "email"]becomesemail). - Laravel-style:
{ message: string, errors?: Record<string, string[]> } - Generic:
{ error: string }or{ errors: string[] }(message is the first entry)
If nothing matches, the message is "Request failed".
Custom error body parser¶
If your backend uses another shape, pass parseErrorBody to createApiClient. Your parser replaces the default one - it is not chained in front of it. Returning null means "no match" and produces the fallback message "Request failed"; the built-in shapes are not tried. To keep them as a fallback, call the exported defaultErrorBodyParser yourself:
import {
createApiClient,
defaultErrorBodyParser,
type ErrorBodyParser,
} from '@msflib/typescript';
const parseErrorBody: ErrorBodyParser = (body, response) => {
const b = body as { title?: string; violations?: Record<string, string[]> };
if (!b?.title) return defaultErrorBodyParser(body, response); // fall back to the built-in shapes
return { message: b.title, errors: b.violations };
};
const { apiClient } = createApiClient({
baseURL: 'https://api.example.com',
parseErrorBody,
});
The parser only runs when the body is valid JSON.
Error events (emitError)¶
createApiClient takes an optional second argument, an emitError callback of type ClientErrorDispatch. It is called in addition to the error being thrown - useful for global toasts, logging or analytics.
const { apiClient } = createApiClient(
{ baseURL: 'https://api.example.com' },
(eventName, eventType, detail) => {
// eventName: 'api-response-error' | 'network-error'
// eventType: 'ApiError' | 'NetworkError'
console.error(eventName, detail);
},
);
eventName |
eventType |
detail |
Fires when |
|---|---|---|---|
'api-response-error' |
'ApiError' |
ApiErrorDetail |
Non-2xx response with a JSON body |
'network-error' |
'NetworkError' |
NetworkErrorDetail |
fetch rejects |
ApiErrorDetail.message is the parsed message (the real one, even for 5xx). loc and type are taken from the first FastAPI detail item when present, otherwise [] and ''.
Low-level helpers¶
These are what createApiClient uses internally. You only need them if you are building your own fetch wrapper.
makeResponseError(
response: Response,
emitError?: ClientErrorDispatch,
parseErrorBody?: ErrorBodyParser, // defaults to defaultErrorBodyParser
): Promise<UserError>; // always resolves to an ApiResponseError
makeNetworkError(
error: Error,
emitError?: ClientErrorDispatch,
): Promise<UserError>;
defaultErrorBodyParser: ErrorBodyParser;
Types¶
interface ParsedApiError {
message: string;
errors?: Record<string, string[]>;
}
// Return null for "this shape doesn't match"
type ErrorBodyParser = (body: unknown, response: Response) => ParsedApiError | null;
type ClientErrorDispatch = (
eventName: string,
eventType: 'ApiError' | 'NetworkError',
detail: ApiErrorDetail | NetworkErrorDetail,
) => void;
interface ApiErrorDetail {
status: number;
statusText: string;
message: string;
loc?: unknown[];
type?: string;
errors?: Record<string, string[]>;
raw?: unknown;
}
interface NetworkErrorDetail {
name: string;
message: string;
cause?: unknown;
stack?: string;
}
See also: HTTP client · Quick Start · Overview