Classify Errors Into Retryable And Fatal

An error classifier that maps thrown errors to retryable or fatal decisions with an explicit reason, so retry policy never guesses from message text.

TypescriptResilience

Code

export type ErrorKind = "retryable" | "fatal" | "unknown";

export interface ErrorDecision {
  readonly kind: ErrorKind;
  readonly retryable: boolean;
  readonly reason: string;
}

const RETRYABLE_CODES = new Set(["ECONNRESET", "ETIMEDOUT", "EAI_AGAIN", "EPIPE"]);

const FATAL_CODES = new Set(["EACCES", "ENOENT", "EINVAL"]);

function statusOf(error: unknown): number | null {
  if (!error || typeof error !== "object") return null;
  const record = error as Record<string, unknown>;
  const status = record.status ?? record.statusCode;
  return typeof status === "number" ? status : null;
}

function codeOf(error: unknown): string | null {
  if (!error || typeof error !== "object") return null;
  const record = error as Record<string, unknown>;
  return typeof record.code === "string" ? record.code : null;
}

export function classifyError(error: unknown): ErrorDecision {
  const code = codeOf(error);
  if (code && RETRYABLE_CODES.has(code)) {
    return { kind: "retryable", retryable: true, reason: `transient system error code ${code}` };
  }
  if (code && FATAL_CODES.has(code)) {
    return { kind: "fatal", retryable: false, reason: `local error code ${code} will not fix itself` };
  }
  const status = statusOf(error);
  if (status !== null) {
    if (status >= 500) {
      return { kind: "retryable", retryable: true, reason: `server status ${status} is transient` };
    }
    if (status >= 400) {
      return { kind: "fatal", retryable: false, reason: `client status ${status} will fail again` };
    }
  }
  return {
    kind: "unknown",
    retryable: false,
    reason: "no recognised code or status; defaulting to non-retryable",
  };
}