Errors#
Error handling in TypeScript is JavaScript’s throw /
try / catch (see
Errors) with a type layer on
top. The catch binding is unknown by default in modern
tsconfig; the operator narrows it before use, or returns a
Result<T, E> structure to encode failure in the return type
instead of throwing.
Catch is unknown#
With useUnknownInCatchVariables (on by default under
strict) the catch parameter has type unknown. The
operator narrows before reading any properties.
try {
await fetchUser(id);
} catch (e) {
if (e instanceof Error) console.error(e.message);
else console.error("unknown:", e);
}
The older default was any, which let e.message go
through unchecked and bit operators when the thrown value was a
string or a plain object.
Typed Error subclasses#
Subclass Error and set name; the operator carries extra
properties on the subclass for the catcher to read.
class HTTPError extends Error {
constructor(
public readonly status: number,
public readonly body: unknown,
) {
super(`HTTP ${status}`);
this.name = "HTTPError";
}
}
try { await call(); }
catch (e) {
if (e instanceof HTTPError && e.status === 401) refresh();
else throw e;
}
The class machinery is JavaScript; the type layer is in the
readonly parameter properties.
Result types#
The operator’s alternative to throwing is to return a
Result<T, E> discriminated union. The compiler forces the
caller to handle both branches.
type Result<T, E = Error> =
| {ok: true; value: T}
| {ok: false; error: E};
async function parseConfig(raw: string): Promise<Result<Config, string>> {
try {
const data = JSON.parse(raw);
return {ok: true, value: data};
} catch (e) {
return {ok: false, error: `bad JSON: ${(e as Error).message}`};
}
}
const r = await parseConfig(raw);
if (r.ok) handle(r.value);
else log.warn(r.error);
For more ceremony (combinators, async pipelines), libraries like
neverthrow and effect provide Result / Effect
structures with chaining APIs.
zod for input validation#
Do not trust JSON. zod parses and validates
in one step; failures are a typed ZodError.
import {z} from "zod";
const ConfigSchema = z.object({
host: z.string(),
port: z.number().int().positive(),
});
type Config = z.infer<typeof ConfigSchema>;
const result = ConfigSchema.safeParse(JSON.parse(raw));
if (!result.success) {
console.error("invalid config:", result.error.flatten());
return;
}
const config: Config = result.data;
safeParse returns a discriminated union; parse throws.
Prefer safeParse for user-facing input and
parse for internal trust boundaries.
Exhaustive catch with never#
When you want to prove every error variant is handled,
narrow with a discriminated union and a never fallback.
type AppError =
| {kind: "Network"; cause: Error}
| {kind: "NotFound"; id: string}
| {kind: "Forbidden"};
function describe(e: AppError): string {
switch (e.kind) {
case "Network": return `net: ${e.cause.message}`;
case "NotFound": return `not found: ${e.id}`;
case "Forbidden": return "forbidden";
default: {
const _exhaustive: never = e;
throw new Error(`unhandled: ${JSON.stringify(_exhaustive)}`);
}
}
}
Async errors#
A rejected promise that nothing handles becomes an
unhandledRejection; in Node this aborts the process by
default. Always handle at the top of an async call chain.
try {
const r = await fetch(url);
if (!r.ok) throw new HTTPError(r.status, await r.text());
return await r.json();
} catch (e) {
log.error(e);
throw e;
}
Promise.all rejects on the first failure; Promise.allSettled
returns {status, value} / {status, reason} per input.
AggregateError (Promise.any) carries multiple errors.
const results = await Promise.allSettled(urls.map(fetch));
const failures = results.flatMap(r => r.status === "rejected" ? [r.reason] : []);
Wrapping with cause#
Modern Error accepts {cause} to carry the original
without losing the stack.
try { await load(id); }
catch (e) {
throw new Error(`load ${id} failed`, {cause: e});
}
Typed wrappers preserve narrow types on the cause.
class ServiceError<E extends Error> extends Error {
constructor(msg: string, public readonly cause: E) {
super(msg, {cause});
this.name = "ServiceError";
}
}
Assertions#
assert returns asserts cond so the compiler narrows
after the call.
function assert(cond: unknown, msg = "assertion failed"): asserts cond {
if (!cond) throw new Error(msg);
}
function process(req: Request | null) {
assert(req, "req must be present");
req.headers; // req: Request from here on
}
References#
Errors for the underlying
throw/trymachinery.Types for
Resultstructures, discriminated unions, andnever.Functions for
assertssignatures.zod, runtime validation that yields TypeScript types.
neverthrow,
ResultandResultAsyncfor async chains.