cligentic
← back to catalog
cligentic block

error-map

Typed AppError class with code, name, human message, and hint. Maps upstream API errors to actionable messages. Agents read the hint field to self-correct.

Install
error-map demo
NPM dependencies

zero deps — pure TS

Block dependencies

standalone

Size

72 LOC

What it does

Maps upstream API error codes to typed AppError objects with a code, human-readable message, and an actionable hint. Agents read the hint field to self-correct in one iteration instead of guessing.

Quickstart

import { AppError, mapError } from "@/cli/foundation/error-map";
 
const errors = {
  AUTH_EXPIRED: {
    name: "AuthExpired",
    human: "Session expired.",
    hint: "Run: myapp login",
  },
  RATE_LIMIT: {
    name: "RateLimit",
    human: "Too many requests.",
    hint: "Wait 60s and retry.",
  },
};
 
try {
  await api.call();
} catch (err) {
  throw mapError(err, errors);
}

The AppError class

class AppError extends Error {
  code: string;
  human: string;
  hint?: string;
 
  toJSON(): { ok: false; code: string; error: string; hint?: string }
}

toJSON() makes it work with json-mode's reportError(). The structured output includes the hint so agents can parse it and act.

API

mapError(err: unknown, errors: ErrorMap): AppError

If the error has a .code property matching a key in the map, returns the mapped AppError. Otherwise wraps the original error as UNKNOWN.

Why hints matter for agents

Without a hint, an agent that encounters "Session expired" has to re-derive "I should run the login command" from context. With a hint, the agent reads "Run: myapp login" and executes it. One fewer reasoning step. Faster recovery.

Source

The full file, 72 lines.

This is the exact TypeScript that lands in your project when you run the install command. Read it, copy it, edit it, own it. cligentic never touches it again.

src/cli/foundation/error-map.ts
// cligentic block: error-map
//
// Typed error class with code, name, human message, and hint. Maps
// upstream API errors to actionable messages. Agents read the hint
// field to self-correct.
//
// Usage:
//   import { AppError, mapError } from "./foundation/error-map";
//
//   const errors = {
//     "AUTH_EXPIRED": { name: "AuthExpired", human: "Session expired.", hint: "Run: myapp login" },
//     "RATE_LIMIT":  { name: "RateLimit", human: "Too many requests.", hint: "Wait 60s and retry." },
//   };
//
//   try { await api.call(); }
//   catch (err) { throw mapError(err, errors); }

export type ErrorEntry = {
  name: string;
  human: string;
  hint?: string;
};

export type ErrorMap = Record<string, ErrorEntry>;

export class AppError extends Error {
  code: string;
  human: string;
  hint?: string;

  constructor(code: string, entry: ErrorEntry, cause?: unknown) {
    super(entry.human);
    this.name = entry.name;
    this.code = code;
    this.human = entry.human;
    this.hint = entry.hint;
    if (cause) this.cause = cause;
  }

  toJSON() {
    return {
      ok: false,
      code: this.code,
      name: this.name,
      error: this.human,
      hint: this.hint,
    };
  }
}

/**
 * Maps an upstream error to an AppError using the provided error map.
 * If the error has a `code` property that matches a key in the map,
 * returns a typed AppError with the mapped message and hint.
 * Otherwise wraps the original error as-is.
 */
export function mapError(err: unknown, errors: ErrorMap): AppError {
  const code = extractCode(err);
  if (code && errors[code]) {
    return new AppError(code, errors[code], err);
  }
  const message = err instanceof Error ? err.message : String(err);
  return new AppError("UNKNOWN", { name: "UnknownError", human: message }, err);
}

function extractCode(err: unknown): string | null {
  if (err && typeof err === "object" && "code" in err) {
    return String((err as Record<string, unknown>).code);
  }
  return null;
}