universal-api-errors

Error types

The ErrorType union, validation shape, and every boolean flag on UniversalError.

type

A small, closed set — deliberately coarse. Finer distinctions that don't map to a single HTTP status range (CORS, DNS, SSL, offline) are exposed as boolean flags instead, so type stays a stable switch/case target while the flags can grow without breaking it.

typeTypically means
"Unauthorized"401
"Forbidden"403
"NotFound"404
"Timeout"408, or a network-level timeout with no status
"Validation"400 or 422
"RateLimit"429
"Server"500+
"Network"Request never got a response — offline, DNS failure, connection refused
"Cancelled"The request was aborted
"Unknown"Nothing matched

When a status code is present, it's authoritative (statusToType). When there's no status at all — the request never got a response — resolveErrorType falls back to Node error codes (ECONNREFUSED, ETIMEDOUT, ...) and browser fetch's message family ("Failed to fetch", Safari's "Load failed", Firefox's "NetworkError when attempting to fetch").

Boolean flags

error.isUnauthorized; // type === "Unauthorized" || status === 401
error.isForbidden; // type === "Forbidden" || status === 403
error.isNotFound; // type === "NotFound" || status === 404
error.isValidation; // type === "Validation", OR real field-level validation data is present
error.isRateLimited; // type === "RateLimit" || status === 429
error.isServer; // type === "Server" || status >= 500
error.isNetwork; // type === "Network", or offline/DNS/connection-refused signals
error.isOffline; // navigator-offline hint, or "offline"/"no internet" in the message
error.isTimeout; // ETIMEDOUT-family codes, or "timeout"/"timed out" in the message
error.isCancelled; // AbortController signal, ERR_CANCELED/ABORT_ERR, or "aborted" in the message
error.isDNS; // ENOTFOUND, EAI_AGAIN
error.isSSL; // certificate/TLS error codes or message
error.isCors; // "cors" or "cross-origin ... block" in the message

DNS/SSL/CORS/offline/cancelled detection is heuristic by nature — there's no HTTP status code for "your DNS resolver failed" — so these lean on Node error codes and message-substring matching. Adapters with a better signal (e.g. a real AbortSignal.aborted) pass that through as a hint rather than relying purely on message sniffing.

isValidation is the one flag with two independent triggers. Most flags are pure functions of type/status; isValidation is also true whenever real field-level validation data was found in the body — even with no status code confirming it. That matters for a body with no HTTP envelope at all, like a bare { message, errors: { email: [...] } } with no status anywhere: type still resolves to "Unknown" (nothing there maps to a status range), but isValidation is true because error.validation actually has field data. Try it in the Playground with the "Laravel-style validation" preset.

This is deliberately narrower than "any errors field": a GraphQL/JSON:API-style body like { errors: [{ message: "..." }] } has no field name to attach the message to, so it normalizes into { _general: [...] } — and that alone does not flip isValidation. Only a real field key does. Try the "Generic errors array" preset in the Playground to see the difference.

retryable

Defaults to true for "Network", "Timeout", and "RateLimit", and for "Server" errors except 501 Not Implemented and 505 HTTP Version Not Supported (neither succeeds on retry). Everything else defaults to false. Override per-error via flagOverrides, or check dynamically with error.shouldRetry(predicate).

validation

type ValidationErrors = Record<string, string[]>;

Field name → list of messages, regardless of source. Produced by normalizeValidation, which folds Laravel, Rails, express-validator, and FastAPI's different shapes into this one. Use "_general" as the key for errors that aren't tied to a specific field.

source

Where the error was produced from — "axios" and "fetch" are what the shipped adapters set today; parseError() alone reports "unknown" when it can't tell. The type also reserves values for adapters that don't exist yet ("graphql", "react-query", "node", "express", "nestjs", ...) — see the roadmap for what's actually planned.

On this page