Skip to content

Error Contract

The error contract defines how products communicate failures to users and machines.

Every user-facing error must emit this shape:

interface StructuredError {
code: string; // e.g. "INPUT_TEXT_EMPTY"
message: string; // human-readable explanation
hint: string; // actionable guidance
cause?: string; // upstream error (if any)
retryable?: boolean;
}

This is the minimum bar. Libraries, CLIs, MCP servers, desktop apps — all user-facing errors must have code, message, and hint.

Tier 2 — Base type + exit codes (CLI/MCP/desktop)

Section titled “Tier 2 — Base type + exit codes (CLI/MCP/desktop)”

For CLI tools, MCP servers, and desktop apps, add a typed error class with:

  • Safe output mode (strips internal details before showing to user)
  • Debug output mode (full details for developers)
  • Exit codes:
Exit codeMeaning
0OK
1User error (bad input, missing config)
2Runtime error (crash, backend failure)
3Partial success (some items succeeded)

Codes use namespaced prefixes to group related failures:

PrefixDomain
IO_File system, network I/O
CONFIG_Configuration errors
PERM_Permission denied
DEP_Missing or incompatible dependencies
RUNTIME_Unexpected runtime failures
PARTIAL_Some items succeeded, some failed
INPUT_User input validation
STATE_Invalid state transitions

Error codes are stable once released — never change the meaning of an existing code.

class AppError extends Error {
constructor(
public code: string,
message: string,
public hint: string,
public cause?: string,
public retryable = false,
) {
super(message);
this.name = 'AppError';
}
toSafe() {
return { code: this.code, message: this.message, hint: this.hint };
}
toDebug() {
return { ...this.toSafe(), cause: this.cause, retryable: this.retryable };
}
}