Everyday TypeScript: Exceptions
Welcome to the Exceptions lesson!
This lesson is shown as static text below. However, it's designed to be used interactively. Click the button below to start!
We've seen many places where TypeScript checks our types: variables, function arguments, object properties, etc. In this lesson, we'll explore one place where TypeScript can't help us much: exceptions.
The code below intentionally throws an
Error. We try tocatchthis error with the TypeScriptErrortype, but it doesn't work. In fact, our attempt to catch theErroris itself a type error!>
try {throw new Error('something failed');} catch (e: Error) {console.log('we caught the exception');}Result:
type error: Catch clause variable type annotation must be 'any' or 'unknown' if specified.
We can never say
catch (e: Error). As the error message says, we can onlycatch (e: any)orcatch (e: unknown).When using TypeScript, we need to work around this limitation. The type error message above spells out two choices for navigating this.
The first choice is to catch
any. As always, we lose type safety if we useany.JavaScript and TypeScript allow us to throw any value, not just instances of
Error. We can throw a string, or a number, ornull, or anything else. If wecatch (e: any), then the actual values at runtime may not match the static types.>
let error: Error;try {throw 'this is not an error object';} catch (e: any) {error = e;}error;Result:
'this is not an error object'
The types there were wrong: our variable was supposed to hold an
Error, but it held a string. Usinganyhere (or almost anywhere else!) risks introducing this kind of bug.Our other type option when catching is
unknown. This is safer because it forces us to narrow the type with type guards.The next two examples are similar, but only one of them uses a type guard on the caught exception. The type guard version works. Without a type guard, we get a type error.
>
let error: Error;try {throw new Error('something broke');} catch (e: unknown) {error = e;}error;Result:
type error: Type 'unknown' is not assignable to type 'Error'.
>
let error: Error;try {throw new Error('something broke');} catch (e: unknown) {/* We can only catch exceptions as `unknown` or `any`. We use `unknown`* because it's safer, but then we have to narrow the type with a* conditional. */if (e instanceof Error) {error = e;} else {throw new Error("We can't handle that type of exception!");}}error.message;Result:
'something broke'
That example shows our recommended solution for exceptions in TypeScript. We recommend catching the error as
catch (e: unknown), then usinge instanceof OurErrorClassas a type guard. Fortunately, this works well with custom error classes, as long as we make sure to inherit fromError.Here's a code problem:
The code below catches an exception and tries to store it in the
errorvariable with the typeUserDoesNotExistError. Currently, it causes a type error, because we can't assign anunknownto aUserDoesNotExistError. Add anifaround theerror = eassignment, acting as a type guard.You can check for whether the error is an instance of
UserDoesNotExistErrorwithe instanceof UserDoesNotExistError. Make sure to add anelseclause that throws when the type guard returnsfalse. Otherwise, you'll probably get a type error saying thaterrorwas "used before being assigned".class UserDoesNotExistError extends Error {userId: number;constructor(userId: number) {super();this.userId = userId;}}let error: UserDoesNotExistError;try {throw new UserDoesNotExistError(57);} catch (e: unknown) {if (e instanceof UserDoesNotExistError) {error = e;} else {throw new Error("We can't handle that type of exception!");}}error.userId;- Goal:
57
- Yours:
57
This course has been full of good news: lesson after lesson, we've shown ways to add static types that prevent bugs before they happen. Unfortunately, this lesson was mostly bad news. TypeScript isn't good at statically typing exceptions, but we can work around that with type guards like
instanceof. Fortunately,catchis relatively uncommon, so this is only a mild inconvenience in practice.