TypeScript 68 ๐ท Error Handling and Typed Errors
Error handling in TypeScript has a specific problem: JavaScript can throw anything โ a string, a number, an object, an Error โ and the type system does not track what a function throws. There is no throws clause, no error channel in the return type. The try/catch block’s catch variable is unknown in strict mode, and every throw is a separate path that the compiler cannot verify. The result is that error handling is the part of the code that is most often untyped, and the bugs that come from the untyped handling are the ones that appear in production. This chapter covers the typing of errors: the unknown catch variable, the type guards, the custom error classes, the Result and Either patterns, the error’s narrowing, the cause property, the AggregateError, and the patterns that make error handling as type-safe as the rest of the code. It builds on the Promise and async/await material from TypeScript 63 and the validation from TypeScript 54.
Key point: The catch variable is unknown in strict mode, which forces the narrowing. The instanceof Error guard is the common, and the custom type guards handle the specific error classes. The custom error class extends the Error and adds the specific properties. The Result<T, E> and the Either<L, R> patterns track the error type in the return type, which makes the error handling explicit. The Error‘s cause property carries the underlying error. The AggregateError groups the multiple errors. The error handling is the discipline, and the discipline is the type safety.
The problem with the untyped errors
The JavaScript’s throw can throw any value, and the catch receives it. The type system does not track the throws, and the compiler cannot verify the handling.
The thrown value’s type. The throw new Error('x') throws an Error, but the throw 'x' throws a string, and the throw { code: 1 } throws the object. The runtime allows all of them, and the type system does not know.
function risky(): void {
throw 'a string';
}
try {
risky();
} catch (error) {
// error is unknown in strict mode
}
The error is unknown, and the instanceof Error guard does not match the string. The string error is the surprise, and the surprise is the problem.
Why the runtime is the loose. The JavaScript’s throw is the loose, and the language does not require the Error. The loose is the flexibility, and the flexibility is the danger. The Error is the convention, and the convention is the discipline.
Why the catch is the unknown. The useUnknownInCatchVariables: true (implied by the strict) makes the catch variable the unknown. The unknown forces the narrowing, and the narrowing is the safety.
Why the any was the old default. The older TypeScript made the catch variable the any, which did not force the narrowing. The any was the convenience, and the convenience was the danger. The unknown is the modern, and the modern is the safety.
Why the thrown type matters. The thrown type determines the handling. The Error has the message and the stack, the string has the length, and the object has the properties. The wrong assumption is the bug, and the assumption is the risk.
Why the errors are the untracked. The type system does not have the throws clause, and the function’s error is not in the return type. The Result<T, E> is the alternative, and the alternative is the explicit. The two are the choice, and the choice is the design.
Why the error handling is the hard. The error handling is the hard because the errors are the unexpected, and the unexpected is the untracked. The discipline is the narrowing, and the narrowing is the safety. The discipline is the skill, and the skill is the practice.
Why the
unknownis the correct default. Theunknownis the top type, and it accepts the any. Theunknownrequires the narrowing, and the narrowing is the safe. Theanyis the unsafe, and the unsafe is the legacy. Theunknownis the modern, and the modern is the correct.
The custom error classes
The custom error class extends the Error and adds the specific properties. The class is the typed, and the class is the identifiable.
The basic custom error. The class extends the Error and sets the name and the message.
class ApiError extends Error {
constructor(
readonly status: number,
readonly code: string,
message: string,
) {
super(message);
this.name = 'ApiError';
}
}
The status and the code are the readonly properties, and the message is the base’s. The name is the class’s, and the class is the identifiable.
Why the name is set. The Error‘s name is the 'Error' by default, and the custom sets the class’s name. The name is the string, and the instanceof is the reliable. The name is the debugging, and the debugging is the value.
Why the super(message) is called. The super(message) sets the base’s message, and the base’s stack is the current. The super is the first, and the first is the requirement. The super‘s call is the error’s initialization, and the initialization is the correct.
The instanceof‘s reliability. The error instanceof ApiError is the reliable, and the error.name === 'ApiError' is the alternative. The instanceof is the preferred, and the preferred is the type guard.
try {
await fetchUser('1');
} catch (error) {
if (error instanceof ApiError) {
console.log(error.status); // the number
console.log(error.code); // the string
}
}
The instanceof ApiError narrows the error to the ApiError, and the status and the code are the available. The narrowing is the type guard, and the guard is the safety.
Why the instanceof works across the modules. The instanceof checks the prototype chain, and the class’s prototype is the same across the modules if the class is the single. The Symbol.hasInstance is the custom, and the custom is the advanced. The instanceof is the common, and the common is the reliable.
The error’s hierarchy. The custom errors can extend the other custom errors, and the hierarchy is the organization.
class HttpError extends Error {
constructor(readonly status: number, message: string) {
super(message);
this.name = 'HttpError';
}
}
class NotFoundError extends HttpError {
constructor(message: string) {
super(404, message);
this.name = 'NotFoundError';
}
}
class ValidationError extends Error {
constructor(readonly fields: Record<string, string>) {
super('Validation failed');
this.name = 'ValidationError';
}
}
The HttpError is the base, and the NotFoundError is the specific. The ValidationError is the other branch, and the two are the different. The hierarchy is the organization, and the organization is the handling.
Why the hierarchy matters. The hierarchy determines the catch’s specificity. The catch (error) { if (error instanceof NotFoundError) } catches the specific, and the if (error instanceof HttpError) catches the broader. The hierarchy is the organization, and the order is the specific first.
Why the custom errors matter. The custom errors are the typed, and the typed is the safety. The instanceof is the narrowing, and the narrowing is the access. The custom is the convention, and the convention is the discipline.
The type guards
The type guard narrows the unknown to the specific. The instanceof is the common, and the custom is the specific.
The instanceof guard. The instanceof narrows the unknown to the class.
function handleError(error: unknown): void {
if (error instanceof Error) {
console.error(error.message);
}
}
The error instanceof Error narrows the unknown to the Error, and the message is the available. The instanceof is the common, and the common is the built-in.
The custom type guard. The custom type guard is the function with the is predicate.
function isApiError(error: unknown): error is ApiError {
return error instanceof ApiError;
}
function handleError(error: unknown): void {
if (isApiError(error)) {
console.error(error.status);
}
}
The error is ApiError is the type predicate, and the isApiError is the guard. The custom is the reusable, and the reusable is the value.
Why the custom guard is the reusable. The custom guard is the function, and the function is the reusable. The instanceof is the inline, and the custom is the function. The two are the choice, and the choice is the style.
The duck-typing guard. The duck-typing guard checks the shape, not the class.
function isErrorWithMessage(error: unknown): error is { message: string } {
return (
typeof error === 'object' &&
error !== null &&
'message' in error &&
typeof (error as { message: unknown }).message === 'string'
);
}
The guard checks the message‘s presence and the type. The duck-typing is the structural, and the structural is the flexible. The guard is the safety, and the safety is the value.
Why the duck-typing guard matters. The duck-typing guard handles the errors that are not the Error‘s instances. The cross-realm’s Error is the example, and the instanceof fails across the realms. The duck-typing is the alternative, and the alternative is the robust.
The Error‘s cause guard. The cause is the ES2022’s, and the guard checks the presence.
function hasCause(error: unknown): error is Error & { cause: unknown } {
return error instanceof Error && 'cause' in error;
}
The guard checks the Error and the cause. The cause is the underlying, and the guard is the access.
Why the cause matters. The cause is the underlying error, and the new Error('outer', { cause: inner }) is the pattern. The cause is the chain, and the chain is the debugging. The cause is the modern, and the modern is the ES2022.
Why the type guards matter. The type guards are the narrowing, and the narrowing is the safety. The instanceof, the custom, the duck-typing, and the cause are the four. The guards are the vocabulary, and the vocabulary is the fluency.
The Result pattern
The Result<T, E> pattern tracks the error type in the return type, which makes the error handling explicit. The function returns the Result, and the caller narrows the ok.
The Result‘s type. The Result is the discriminated union of the success and the failure.
type Result<T, E> = { ok: true; value: T } | { ok: false; error: E };
The ok is the discriminant, and the value and the error are the payloads. The union is the two, and the two are the exclusive.
The function’s return. The function returns the Result, and the error is the return value.
async function getUser(id: string): Promise<Result<User, ApiError>> {
try {
const user = await fetchUser(id);
return { ok: true, value: user };
} catch (error) {
if (isApiError(error)) {
return { ok: false, error };
}
return { ok: false, error: new ApiError(500, 'UNKNOWN', 'Unknown error') };
}
}
The getUser returns the Result<User, ApiError>, and the two branches are the ok. The error is the return value, and the return is the explicit.
Why the Result is the explicit. The Result tracks the error in the type, and the caller must handle the two branches. The try/catch does not track the error, and the caller can ignore it. The Result is the explicit, and the explicit is the safety.
The caller’s handling. The caller narrows the ok and handles the two branches.
const result = await getUser('1');
if (result.ok) {
console.log(result.value.name);
} else {
console.log(result.error.status);
}
The if (result.ok) narrows the result to the success, and the else is the failure. The result.value and the result.error are the typed, and the typed is the access.
Why the caller’s handling is the safe. The caller must handle the two branches, and the compiler enforces it. The result.value without the if (result.ok) is the compile error, and the error is the enforcement.
The Either‘s type. The Either<L, R> is the functional’s alternative, and the Left and the Right are the two.
type Either<L, R> = { _tag: 'Left'; left: L } | { _tag: 'Right'; right: R };
The _tag is the discriminant, and the Left and the Right are the payloads. The Either is the functional’s, and the Result is the object’s. The two are the same, and the naming is the difference.
Why the Either matters. The Either is the fp-ts’s, and the functional’s. The Result is the object’s, and the two are the equivalent. The choice is the style, and the style is the team’s.
Why the Result should be the choice. The Result is the explicit, and the explicit is the safety. The exceptions are the default, and the Result is the alternative. The two are the choice, and the choice is the design.
The AggregateError
The AggregateError groups the multiple errors, and it is the ES2021’s. The errors property is the array, and the message is the summary.
const errors = [
new Error('First failure'),
new Error('Second failure'),
];
throw new AggregateError(errors, 'Multiple failures occurred');
The errors is the array, and the message is the summary. The AggregateError is the group, and the group is the multiple.
Why the AggregateError matters. The AggregateError is the group, and the group is the multiple. The Promise.any uses the AggregateError when all the promises reject, and the errors are the individual. The group is the modern, and the modern is the ES2021.
The Promise.any‘s error. The Promise.any rejects with the AggregateError, and the errors are the individual rejections.
try {
const result = await Promise.any([fetchFrom1(), fetchFrom2()]);
} catch (error) {
if (error instanceof AggregateError) {
for (const e of error.errors) {
console.error(e);
}
}
}
The error.errors is the array, and the for iterates. The AggregateError is the group, and the group is the multiple.
Why the AggregateError should be the checked. The AggregateError‘s errors is the array, and the array is the individual. The errors.length is the count, and the count is the summary. The check is the requirement, and the requirement is the safety.
The errors‘ typing. The errors is the any[], and the individual’s type is the lost. The custom’s AggregateError<Error> is the generic’s, and the generic is the modern.
class AggregateError<T = Error> extends Error {
constructor(readonly errors: T[], message: string) {
super(message);
this.name = 'AggregateError';
}
}
The AggregateError<T> is the generic, and the errors is the T[]. The generic is the typed, and the typed is the safety.
Why the generic’s AggregateError matters. The generic’s AggregateError is the typed, and the errors is the T[]. The typed is the specific, and the specific is the access. The generic is the modern, and the modern is the safety.
Why the AggregateError is the specific. The AggregateError is the group, and the group is the multiple. The Promise.any is the use, and the use is the common. The AggregateError is the specific, and the specific is the group.
The try/catch‘s patterns
The try/catch has the patterns, and each has the use. The patterns are the vocabulary, and the vocabulary is the fluency.
The narrow the catch. The catch’s unknown is the narrowed, and the narrow is the access.
try {
await risky();
} catch (error) {
if (error instanceof Error) {
console.error(error.message);
} else {
console.error('Unknown error', error);
}
}
The instanceof Error is the narrow, and the else is the fallback. The narrow is the access, and the fallback is the safety.
Why the narrow matters. The narrow is the access, and the access is the property. The error.message is the Error‘s, and the narrow is the requirement. The narrow is the safety, and the safety is the value.
The re-throw. The catch’s re-throw is the error’s propagation, and the re-throw is the pattern.
try {
await risky();
} catch (error) {
console.error('Failed', error);
throw error; // the re-throw
}
The throw error is the re-throw, and the re-throw is the propagation. The log is the side effect, and the re-throw is the error’s continuation.
Why the re-throw matters. The re-throw is the propagation, and the propagation is the caller’s. The caller’s handling is the higher, and the higher is the abstraction. The re-throw is the pattern, and the pattern is the layered.
The wrap. The catch’s wrap is the error’s transformation, and the wrap is the pattern.
try {
await risky();
} catch (error) {
throw new ApiError(500, 'RISKY_FAILED', 'Risky operation failed', { cause: error });
}
The new ApiError(..., { cause: error }) is the wrap, and the cause is the original. The wrap is the transformation, and the transformation is the abstraction.
Why the wrap matters. The wrap is the abstraction, and the abstraction is the caller’s. The cause is the original, and the original is the debugging. The wrap is the pattern, and the pattern is the modern.
The finally‘s cleanup. The finally is the cleanup, and the cleanup is the guarantee.
let connection: Connection | undefined;
try {
connection = await openConnection();
await connection.query('...');
} catch (error) {
console.error('Query failed', error);
} finally {
await connection?.close();
}
The finally is the cleanup, and the cleanup is the both’s. The success and the failure are the both, and the both is the cleanup. The cleanup is the guarantee, and the guarantee is the value.
Why the finally matters. The finally is the cleanup, and the cleanup is the resource’s. The connection, the file, the lock are the resources, and the resources need the release. The finally is the guarantee, and the guarantee is the safety.
Why the patterns matter. The patterns are the vocabulary, and the vocabulary is the fluency. The narrow, the re-throw, the wrap, and the finally are the four. The patterns are the skill, and the skill is the practice.
Complete Example Session
// ============================================
// PART 1: THE UNKNOWN CATCH
// ============================================
try {
risky();
} catch (error) {
// error is unknown
if (error instanceof Error) {
console.error(error.message);
} else {
console.error('Unknown error', error);
}
}
// ============================================
// PART 2: THE CUSTOM ERROR
// ============================================
class ApiError extends Error {
constructor(
readonly status: number,
readonly code: string,
message: string,
options?: { cause?: unknown },
) {
super(message, options);
this.name = 'ApiError';
}
}
// ============================================
// PART 3: THE CUSTOM GUARD
// ============================================
function isApiError(error: unknown): error is ApiError {
return error instanceof ApiError;
}
try {
await fetchUser('1');
} catch (error) {
if (isApiError(error)) {
console.error(error.status, error.code);
}
}
// ============================================
// PART 4: THE ERROR HIERARCHY
// ============================================
class HttpError extends Error {
constructor(readonly status: number, message: string) {
super(message);
this.name = 'HttpError';
}
}
class NotFoundError extends HttpError {
constructor(message: string) {
super(404, message);
this.name = 'NotFoundError';
}
}
class ValidationError extends Error {
constructor(readonly fields: Record<string, string>) {
super('Validation failed');
this.name = 'ValidationError';
}
}
// ============================================
// PART 5: THE RESULT PATTERN
// ============================================
type Result<T, E> = { ok: true; value: T } | { ok: false; error: E };
async function getUser(id: string): Promise<Result<User, ApiError>> {
try {
const user = await fetchUser(id);
return { ok: true, value: user };
} catch (error) {
if (isApiError(error)) {
return { ok: false, error };
}
return { ok: false, error: new ApiError(500, 'UNKNOWN', 'Unknown error') };
}
}
const result = await getUser('1');
if (result.ok) {
console.log(result.value.name);
} else {
console.log(result.error.status);
}
// ============================================
// PART 6: THE CAUSE
// ============================================
try {
await risky();
} catch (error) {
throw new ApiError(500, 'RISKY_FAILED', 'Risky operation failed', {
cause: error,
});
}
// ============================================
// PART 7: THE AGGREGATE ERROR
// ============================================
try {
const result = await Promise.any([fetchFrom1(), fetchFrom2()]);
} catch (error) {
if (error instanceof AggregateError) {
for (const e of error.errors) {
console.error(e);
}
}
}
// ============================================
// PART 8: THE FINALLY
// ============================================
let connection: Connection | undefined;
try {
connection = await openConnection();
await connection.query('...');
} catch (error) {
console.error('Query failed', error);
} finally {
await connection?.close();
}
// ============================================
// PART 9: THE DUCK-TYPING GUARD
// ============================================
function isErrorWithMessage(error: unknown): error is { message: string } {
return (
typeof error === 'object' &&
error !== null &&
'message' in error &&
typeof (error as { message: unknown }).message === 'string'
);
}
// ============================================
// PART 10: WHAT NOT TO DO
// ============================================
// Don't use any for the catch
catch (error: any) { error.message; } // the untyped
// Don't assume the error is the Error
catch (error) { console.error(error.message); } // the unknown
// Don't throw the string
throw 'failed'; // the untyped, the bad practice
// Don't swallow the error
catch (error) { /* the nothing */ } // the silent
// Don't lose the cause
catch (error) { throw new Error('failed'); } // the cause is lost
// Don't forget the finally
// The resource's leak is the risk.
The ten parts cover the unknown catch, the custom error, the custom guard, the hierarchy, the Result pattern, the cause, the AggregateError, the finally, the duck-typing guard, and the anti-patterns.
Quick Reference
The Catch’s Variable
| The option | The type |
|---|---|
The strict | The unknown |
The useUnknownInCatchVariables: false | The any |
| The explicit | The annotation |
The Type Guards
| The guard | The narrow |
|---|---|
instanceof Error | The Error |
instanceof ApiError | The ApiError |
The custom error is T | The T |
| The duck-typing | The shape |
The Custom Error
| The part | The code |
|---|---|
| The class | class ApiError extends Error |
| The name | this.name = 'ApiError' |
| The super | super(message, options) |
| The properties | The readonly status: number |
The Result Pattern
| The type | The code |
|---|---|
The Result | { ok: true; value: T } | { ok: false; error: E } |
| The success | { ok: true, value: user } |
| The failure | { ok: false, error } |
| The narrow | if (result.ok) |
The Either Pattern
| The type | The code |
|---|---|
The Either | { _tag: 'Left'; left: L } | { _tag: 'Right'; right: R } |
The Left | The failure |
The Right | The success |
The ES2022’s Features
| The feature | The use |
|---|---|
The cause | new Error('msg', { cause: inner }) |
The AggregateError | new AggregateError(errors, 'msg') |
The errors | The error.errors |
The try/catch‘s Patterns
| The pattern | The use |
|---|---|
| The narrow | The instanceof |
| The re-throw | The throw error |
| The wrap | The new Error('msg', { cause: error }) |
The finally | The cleanup |
Best Practices
โ Do This:
// Narrow the catch's unknown
catch (error) {
if (error instanceof Error) console.error(error.message);
} // โ
// Use the custom error
class ApiError extends Error {
constructor(readonly status: number, message: string) {
super(message);
this.name = 'ApiError';
}
} // โ
// Use the custom guard
function isApiError(e: unknown): e is ApiError { return e instanceof ApiError; } // โ
// Use the Result for the explicit
type Result<T, E> = { ok: true; value: T } | { ok: false; error: E }; // โ
// Use the cause
throw new Error('outer', { cause: inner }); // โ
// Use the finally for the cleanup
try { ... } finally { await connection?.close(); } // โ
// Use the AggregateError for the group
throw new AggregateError(errors, 'Multiple failures'); // โ
โ Don’t Do This:
// Don't use any for the catch
catch (error: any) { error.message; } // โ ๏ธ
// Don't assume the error is the Error
catch (error) { console.error(error.message); } // โ ๏ธ
// Don't throw the string
throw 'failed'; // โ ๏ธ
// Don't swallow the error
catch (error) { /* nothing */ } // โ ๏ธ
// Don't lose the cause
catch (error) { throw new Error('failed'); } // โ ๏ธ
// Don't forget the finally
// The resource's leak is the risk. // โ ๏ธ
Common Pitfalls
| Pitfall | Problem | Solution |
|---|---|---|
The any catch | The untyped | The unknown |
The assumed Error | The wrong property | The guard |
| The string throw | The untyped | The Error |
| The swallowed error | The silent | The log |
| The lost cause | The chain broken | The cause |
The missing finally | The resource’s leak | The finally |
The AggregateError‘s check | The unhandled | The instanceof |
| The order’s specificity | The wrong catch | The specific first |
Real-World Examples
1. The unknown catch
catch (error) {
if (error instanceof Error) console.error(error.message);
}
2. The custom error
class ApiError extends Error {
constructor(readonly status: number, message: string) {
super(message);
this.name = 'ApiError';
}
}
3. The custom guard
function isApiError(e: unknown): e is ApiError { return e instanceof ApiError; }
4. The error hierarchy
class NotFoundError extends HttpError {
constructor(message: string) { super(404, message); }
}
5. The Result
type Result<T, E> = { ok: true; value: T } | { ok: false; error: E };
6. The narrow
if (result.ok) console.log(result.value);
7. The cause
throw new Error('outer', { cause: inner });
8. The AggregateError
throw new AggregateError(errors, 'Multiple failures');
9. The finally
try { ... } finally { await connection?.close(); }
10. The duck-typing guard
function isErrorWithMessage(e: unknown): e is { message: string } {
return typeof e === 'object' && e !== null && 'message' in e;
}
Visual: The Catch’s Unknown
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ try { โ
โ risky(); โ
โ } catch (error) { โ
โ // error is unknown โ
โ // The narrowing is required. โ
โ โ
โ if (error instanceof Error) { โ
โ console.error(error.message); // the access โ
โ } else { โ
โ console.error('Unknown', error); // the fallback โ
โ } โ
โ } โ
โ โ
โ The unknown forces the narrowing, and the narrowing is โ
โ the safety. โ
โ โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
Visual: The Custom Error
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ class ApiError extends Error { โ
โ constructor( โ
โ readonly status: number, โ
โ readonly code: string, โ
โ message: string, โ
โ options?: { cause?: unknown }, โ
โ ) { โ
โ super(message, options); โ
โ this.name = 'ApiError'; โ
โ } โ
โ } โ
โ โ
โ THE INSTANCE โ
โ error.status โ number โ
โ error.code โ string โ
โ error.message โ string โ
โ error.name โ 'ApiError' โ
โ โ
โ The `instanceof ApiError` narrows the unknown to the โ
โ ApiError. โ
โ โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
Visual: The Result Pattern
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ type Result<T, E> = โ
โ | { ok: true; value: T } โ
โ | { ok: false; error: E }; โ
โ โ
โ async function getUser(id: string): Promise<Result<User, ApiError>> {โ
โ try { โ
โ const user = await fetchUser(id); โ
โ return { ok: true, value: user }; โ
โ } catch (error) { โ
โ if (isApiError(error)) { โ
โ return { ok: false, error }; โ
โ } โ
โ return { ok: false, error: new ApiError(500, 'UNKNOWN', 'Unknown') };โ
โ } โ
โ } โ
โ โ
โ THE CALLER โ
โ const result = await getUser('1'); โ
โ if (result.ok) { โ
โ console.log(result.value.name); โ
โ } else { โ
โ console.log(result.error.status); โ
โ } โ
โ โ
โ The two branches are the explicit, and the explicit is โ
โ the safety. โ
โ โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
Visual: The Error Hierarchy
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ Error โ
โ โ โ
โ โโโ HttpError โ
โ โ โ โ
โ โ โโโ NotFoundError (404) โ
โ โ โโโ UnauthorizedError (401) โ
โ โ โโโ ServerError (500) โ
โ โ โ
โ โโโ ValidationError โ
โ โ
โ THE CATCH'S ORDER โ
โ if (error instanceof NotFoundError) { ... } โ
โ else if (error instanceof HttpError) { ... } โ
โ else if (error instanceof Error) { ... } โ
โ โ
โ The specific is the first, and the first is the correct.โ
โ โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
Visual: The try/catch’s Patterns
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ THE NARROW โ
โ catch (error) { โ
โ if (error instanceof Error) { ... } โ
โ } โ
โ โ
โ THE RE-THROW โ
โ catch (error) { โ
โ console.error(error); โ
โ throw error; โ
โ } โ
โ โ
โ THE WRAP โ
โ catch (error) { โ
โ throw new ApiError(500, 'X', 'msg', { cause: error });โ
โ } โ
โ โ
โ THE FINALLY โ
โ try { ... } finally { await close(); } โ
โ โ
โ The four are the vocabulary. โ
โ โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
Visual: The AggregateError
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ try { โ
โ const result = await Promise.any([fetch1(), fetch2()]);โ
โ } catch (error) { โ
โ if (error instanceof AggregateError) { โ
โ for (const e of error.errors) { โ
โ console.error(e); โ
โ } โ
โ } โ
โ } โ
โ โ
โ The AggregateError groups the multiple rejections. โ
โ The `errors` is the array, and the array is the โ
โ individual. โ
โ โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
Summary
| Item | Value |
|---|---|
| The catch’s variable | The unknown |
| The guard | The instanceof Error |
| The custom guard | The error is T |
| The custom error | The class X extends Error |
The Result | The { ok: true; value } | { ok: false; error } |
The Either | The { _tag: 'Left' } | { _tag: 'Right' } |
The cause | The ES2022’s |
The AggregateError | The ES2021’s |
The finally | The cleanup |
| The patterns | The narrow, the re-throw, the wrap, the finally |
Key takeaways:
- The
catchvariable isunknownin strict mode โ it forces the narrowing, and the narrowing is the safety - The
instanceof Errorguard is the common โ the custom type guards handle the specific error classes - The custom error class extends the
Errorand sets thenameโ thesuper(message, options)is the first, and thethis.nameis the class’s - The error hierarchy organizes the handling โ the specific is the first in the catch’s order, and the broad is the fallback
- The
Result<T, E>pattern tracks the error in the return type โ theokis the discriminant, and the two branches are the explicit - The
Either<L, R>is the functional’s alternative โ theLeftand theRightare the two, and the naming is the difference - The
causeproperty carries the underlying error โ thenew Error('outer', { cause: inner })is the ES2022’s, and the chain is the debugging - The
AggregateErrorgroups the multiple errors โ thePromise.anyuses it, and theerrorsis the array - The
finallyblock is the cleanup โ it runs on both the success and the failure, and the resource’s release is the guarantee - The patterns are the vocabulary โ the narrow, the re-throw, the wrap, and the finally are the four, and the four cover the cases
Remember: Error handling in TypeScript is about the unknown and the narrowing. The catch variable is the unknown, the instanceof is the guard, and the custom error is the class. The Result and the Either are the explicit, and the cause and the AggregateError are the ES2022 and ES2021. The finally is the cleanup, and the patterns are the vocabulary. The errors are the untracked, and the tracking is the safety.
Stop using slow, ad-bloated tool sites! ๐คฎ
๐ Search “KandZ Tools” on Google to use many professional utilities for free.
KandZ.me is the ultimate minimalist hub for:
โ
Finance (Mortgage, Interest, Inflation)
โ
Tech (Base64, JSON, Dev Suite, IP)
โ
Health (BMI, BMR, TDEE)
โ
Productivity (Timer, Workspace, QR)
โก๏ธ Fast & Private
๐ No data leaves your device
๐ 100% Free
๐ Use it now: https://tools.kandz.me
๐ Bookmark itโyouโll need it later!