Error handling by failure kind
Rule
Ask whether a correct caller can trigger the failure with ordinary input. If yes (parse, validate, lookup miss), return Result<T, E> and never throw. If no (wrong argument domain, broken invariant), throw a named Error subclass or a built-in such as RangeError. Document thrown errors with @throws.
Do
export type Result<T, E = Error> =
| { readonly ok: true; readonly value: T }
| { readonly ok: false; readonly error: E };
export class QueryParseError extends Error {
override readonly name = 'QueryParseError';
}
export interface Query {
readonly terms: readonly string[];
}
/** Expected failure: user input may contain no terms. */
export function parseQuery(raw: string): Result<Query, QueryParseError> {
const terms = raw
.toLowerCase()
.split(/\s+/)
.filter((t) => t.length > 0);
if (terms.length === 0) {
return { ok: false, error: new QueryParseError('query has no terms') };
}
return { ok: true, value: { terms } };
}
/**
* Contract violation: a negative k1 is a caller bug.
* @throws {RangeError} when k1 is not a finite number >= 0
*/
export function assertK1(k1: number): void {
if (!(Number.isFinite(k1) && k1 >= 0)) {
throw new RangeError(`k1 must be a finite number >= 0, got ${k1}`);
}
}Don’t
// An expected failure that throws forces try/catch on every caller.
export function parseQuery(raw: string): string[] {
const terms = raw.split(/\s+/).filter((t) => t.length > 0);
if (terms.length === 0) throw new Error('empty query');
return terms;
}
// A caller bug wrapped in a Result is easy to ignore.
export function checkK1(k1: number): { ok: boolean } {
return { ok: k1 >= 0 };
}Rationale
Expected failures belong in the type, where the compiler makes callers handle them. Caller bugs should fail loudly at the point of misuse. No lint rule can tell the two kinds apart, so this rule is checked at review stage S2.
Related
- STD-TS-007: the canonical
Resulttext - CGD-05