universal-api-errors

Core API

Everything in @harshilrajput/universal-api-errors-core — re-exported by every adapter.

The core package has zero runtime dependencies, works standalone via duck-typing, and is re-exported in full by both the Axios and fetch adapters — so everything below is available whichever package you install.

parseError

function parseError(input: unknown, options?: { source?: string }): UniversalError

The generic parser. Recognizes Axios/ky/superagent's { response: { status, data } } shape, Node's error codes (ECONNREFUSED, ETIMEDOUT, ...), and the browser fetch network-failure message family — all without importing any of those libraries.

import { parseError } from "@harshilrajput/universal-api-errors-core";

const error = parseError(err);

createUniversalError

function createUniversalError(input: ParsedErrorInput): UniversalError

Builds a fully-classified UniversalError from partial adapter output (message, status, code, validation, ...). This is what every adapter calls once it has extracted those fields from its client's native error shape — it derives type, retryable, and every isX flag consistently, so that logic lives in one place instead of being copy-pasted into every adapter.

The UniversalError class

UniversalError extends the built-in Error — not a plain object — so throw, instanceof Error, console.error, and error-reporting SDKs keep working exactly as they already do.

interface UniversalError extends Error {
  message: string;
  code?: string;
  status?: number;
  type: ErrorType;
  retryable: boolean;
  validation?: { [field: string]: string[] };
  suggestion?: string;
  docs?: string;
  source: string;
  original: unknown;

  isUnauthorized: boolean;
  isForbidden: boolean;
  isNotFound: boolean;
  isValidation: boolean;
  isRateLimited: boolean;
  isServer: boolean;
  isNetwork: boolean;
  isOffline: boolean;
  isTimeout: boolean;
  isCancelled: boolean;
  isDNS: boolean;
  isSSL: boolean;
  isCors: boolean;

  shouldRetry(predicate?: (error: UniversalError) => boolean): boolean;
  debug(): void;
  log(): void;
  toJSON(): Record<string, unknown>;
}

See Error types for what each type and flag actually means.

retry

function retry<T>(fn: () => Promise<T>, options?: RetryOptions): Promise<T>
import { retry } from "@harshilrajput/universal-api-errors-core";

const user = await retry(() => fetchUser(id), {
  retries: 3, // default: 3
  delay: "exponential", // "fixed", "exponential", or a number of ms — default: "exponential"
  jitter: true, // default: true — full jitter, avoids thundering-herd retries
  factor: 2, // exponential multiplier per attempt — default: 2
  maxDelay: 30_000, // upper bound on any computed delay — default: 30s
  shouldRetry: (error, attempt) => true, // decide whether to retry a given error
  onRetry: (error, attempt, delayMs) => {}, // called before each retry sleep
});

computeRetryDelay(attempt, options) is also exported standalone if you want the same backoff curve without the retry loop — it's what UniversalError.shouldRetry() and retry() both use internally.

normalizeValidation

function normalizeValidation(input: unknown): { [field: string]: string[] } | undefined

Folds every validation-error shape this ecosystem produces into one { field: string[] } map:

import { normalizeValidation } from "@harshilrajput/universal-api-errors-core";

normalizeValidation({ email: ["Already exists"] }); // Laravel / Rails
normalizeValidation([{ path: "email", msg: "Already exists" }]); // express-validator
normalizeValidation([{ loc: ["body", "email"], msg: "field required" }]); // FastAPI
// => { email: ["Already exists" | "field required"] } in every case

Logging

function configureLogger(config: LoggerConfig): void
function redact(value: unknown): unknown
function formatDebug(error: UniversalError): string
import { configureLogger } from "@harshilrajput/universal-api-errors-core";

configureLogger({
  redactKeys: ["password", "token", "ssn"], // extends the built-in secret-key list
  redactWith: "[REDACTED]", // default replacement value
  format: "json", // force json instead of auto (pretty outside NODE_ENV=production)
  sink: (payload) => myLogger.error(payload), // route to pino/winston/etc. instead of console
});

error.debug() prints a readable block (status, type, message, source, suggestion); error.log() emits structured output through the same config, redacting secret-looking keys in original by default.

Classification helpers

Lower-level building blocks, exported for adapters and advanced use:

function statusToType(status: number | undefined): ErrorType
function resolveErrorType(status: number | undefined, code: string | undefined, message: string | undefined): ErrorType
function isRetryableByDefault(type: ErrorType, status: number | undefined): boolean
function deriveFlags(input: FlagInput): UniversalErrorFlags

These are what every adapter calls internally — statusToType maps an HTTP status to a coarse ErrorType, resolveErrorType falls back to Node error codes / message sniffing when there's no status at all, and deriveFlags derives the full isX flag set consistently.

On this page