← Fundamentals
Guided lesson 7 questions · TypeScript only

TypeScript catch, unknown, and thrown values

A catch block can receive any value at all. Check what it is before you read from it.

JavaScript lets a program throw any value, not only an Error, and no type says what a function throws. So TypeScript gives a caught value the type unknown, and you check what it is, with instanceof and typeof, before you read its message. The same holds for the reason of a rejected Promise.

Written for TypeScript.

Before you begin

Why this matters

Every catch block you write reads a value that some other code chose to throw. If you check that value before you read it, a failure shows its real reason. If you do not, the screen shows undefined, or the catch block itself throws.

What this helps you reason about

  • Explain why a caught value has the type unknown, and what catch (e: any) and e as Error switch off.
  • Read any caught value safely with instanceof Error, typeof, and a fallback, and write your own error class with a name.
  • Predict whether a rejected Promise reaches a catch block, and spot a return without await inside a try.

In Java or C#, a catch names the class it receives, and only objects of that class arrive. In JavaScript, a throw can carry any value: an Error, a text, a number, or undefined. Errors as values or exceptions showed that a throw ends every call until the nearest catch, and that the catch receives the thrown value. This lesson looks at that value: why TypeScript does not know its type, how to find it out, and what changes when the failure comes from a Promise. The examples come from a fruit stall, with crates on a scale and card payments.

Already know this? The short version
  • throw accepts any value, and nothing in a function’s type says what it throws; under strict (useUnknownInCatchVariables, since TypeScript 4.4) the catch variable is unknown, catch (e: Error) is refused, and catch (e: any) or e as Error switch the checks off, so e.message on a thrown text is undefined.
  • Read a caught value with instanceof Error, then typeof "string", then a fallback; message, name, stack, and cause are only safe after instanceof Error, and cause is unknown again.
  • An Error subclass keeps the name "Error" until its constructor sets this.name; recognize it with instanceof, which also narrows to its fields, and throw on everything you cannot handle.
  • An async function turns a throw into a rejected Promise; await inside a try throws the reason there, but return promise inside a try lets the rejection escape, and the reason in .then and .catch callbacks has the type any.
  • The section “What goes wrong with thrown values in real programs?” covers JSON.stringify on an Error, .catch with an any reason, return without await, and throwing text.

Go to the final check· Go to the practice tickets

Words in this lesson (8)
exception
A value that is thrown to stop the normal flow when an operation fails. In TypeScript, any value can be thrown, but code usually throws an Error object, which has a message.
stack trace
The list of calls that were active when an error was created, printed newest first: the function where the error happened, then the function that called it, down to the top level.
unknown
A type for a value whose type is not known yet. The compiler accepts no use of the value until a check, such as typeof value === "number", has decided its type.
any
The type that turns type checking off: a value of type any may be used as every type, and the compiler checks nothing about it.
instanceof
An operator that tests, while the program runs, whether an object was made by a class or by one of its subclasses, as in problem instanceof Error. The compiler then treats the object as that class.
cause
The original error kept inside a new one, as in new Error("back hedge failed", { cause: problem }). The new error’s cause property gives it back.
Promise
An object that stands for a result that is not ready yet. Later it is fulfilled with a value, or rejected with a reason, which is often an Error.
await
An operator, used inside an async function, that waits for a Promise. It gives the value when the Promise is fulfilled, and throws the reason when the Promise is rejected.

Examples are focused snippets; imports, enclosing functions, or classes may be omitted. C examples use C11; Haskell examples use Haskell 2010. Expand the notes for language-specific depth.

01 / Thrown values

What can a program throw?

A throw ends every call until the nearest catch, and the catch block receives the thrown value, as Errors as values or exceptions showed. That value does not have to be an Error. throw accepts any value: an Error, a text, a number, an object, or undefined. Built-in operations throw Error objects, but other code, often older JavaScript, sometimes throws a text.

An Error object has a message, a name such as "Error" or "TypeError", and a stack trace: the list of calls that were active when the Error was created. A text or a number has none of these.

Two tools work on every value. typeof gives its kind as text: "object" for an Error, "string" for a text, "number" for a number. String(value) turns it into text: an Error becomes its name, a colon, and its message, such as Error: scale stuck, and a text stays itself.

main.ts
function weigh(crate: number): number {
  if (crate === 1) {
    throw new Error("scale stuck");
  }
  if (crate === 2) {
    throw "no crate"; // a text, not an Error
  }
  throw 404; // a number
}

throw accepts any value, so a catch block can receive an Error, a text, a number, or anything else.

Coming from Python: raise "text" is refused in Python 3, because only exceptions can be raised; JavaScript accepts throw "text" without any complaint.

Precisely: what the language allows

The ECMAScript specification puts no limit on the value of a throw statement, so throw undefined and throw null are legal too, and the catch block then receives undefined or null. A text that is thrown has no stack trace, so nothing records where it came from. When no catch receives it, Node.js prints the value with the line of the throw statement, but no list of calls.

Experiment

Catch three kinds of thrown values

throw accepts any value, and the catch block receives exactly that value. typeof tells the kinds apart while the program runs. String turns an Error into its name, a colon, and its message, and turns other values into their usual text.

Why it matters A catch block can receive an Error, a text, a number, or anything else. Code that reads the caught value must be ready for each.

weigh throws a different kind of value for each crate. Predict the three printed lines. Then change line 10 to throw new Error("no crate"); and predict the second line again.

function describe(problem: unknown): string {
  return typeof problem + ": " + String(problem);
}

function weigh(crate: number): number {
  if (crate === 1) {
    throw new Error("scale stuck");
  }
  if (crate === 2) {
    throw "no crate";
  }
  throw 404;
}

for (const crate of [1, 2, 3]) {
  try {
    weigh(crate);
  } catch (problem) {
    console.log(describe(problem));
  }
}
What to look for

On the loop line, crate shows not initialized yet before each pass, and the stepper sometimes stops on one line twice; both are normal. For crate 1, the stop on line 7 is followed by a stop on line 19, in the catch block: line 17 never finishes. In the catch block, the panel lists problem. For crate 1 it shows Error: scale stuck: the panel writes an Error as its name and its message. For crate 2 it shows "no crate", and for crate 3 it shows 404. The run prints object: Error: scale stuck, then string: no crate, then number: 404. After the edit, the second line is object: Error: no crate.

Expected output

object: Error: scale stuck
string: no crate
number: 404

02 / unknown

Why is the caught value unknown?

Nothing in a function’s type says what it may throw, and any function it calls may throw any value. So TypeScript cannot know the type of a caught value. With strict on, as in the stepper, the catch variable has the type unknown. The setting is useUnknownInCatchVariables, part of strict since TypeScript 4.4.

unknown accepts no use until a check. So problem.message is refused, and the compiler reports 'problem' is of type 'unknown'. You cannot choose a type in the catch line either: catch (problem: Error) gives Catch clause variable type annotation must be 'any' or 'unknown' if specified.

Two ways make the error go away, and both are traps. catch (problem: any) turns checking off. (problem as Error).message is a type assertion, as in TypeScript narrowing and discriminated unions: a belief with no test. Both compile, and for a thrown text both read message from a value that has none, which gives undefined. For a thrown undefined, reading .message throws a TypeError inside the catch block itself.

main.ts
try {
  pay(80);
} catch (problem) {
  // refused: 'problem' is of type 'unknown'
  console.log(problem.message);
}

A caught value has the type unknown because anything can be thrown; any and as Error remove the error but keep the problem.

Coming from Java: catch (IOException e) receives only that class; a TypeScript catch receives every thrown value, and the catch line cannot choose a type.

Coming from JavaScript: Code that reads e.message in every catch block stops compiling when it moves to strict TypeScript. That compile error is the point of unknown: it marks each place where a thrown text would give undefined.

Precisely: older code and settings

Before TypeScript 4.4, and today without strict, a catch variable has the type any, so old code often reads e.message with no check. A tsconfig.json can keep strict and set "useUnknownInCatchVariables": false. Since ES2019 you can also write catch { … } with no variable, when the block does not need the value.

Experiment

Read message from a thrown text through any

With : any, the compiler checks nothing about problem, so problem.message compiles. While the program runs, problem is the text "card declined", which has no message property, so reading it gives undefined. Without the annotation, problem has the type unknown, and the compiler refuses .message until a check has decided what problem is.

Why it matters catch (problem: any) and (problem as Error) turn a compile error into undefined at run time. Keep the caught value unknown, and let the compiler ask for the checks.

The card reader throws a text. Predict what the catch block prints. Then delete : any on line 10, so the line reads } catch (problem) {, and predict whether the program still compiles.

function pay(amount: number): void {
  if (amount > 50) {
    throw "card declined";
  }
}

try {
  pay(80);
  console.log("paid");
} catch (problem: any) {
  console.log("failed: " + problem.message);
}
What to look for

Step into pay: amount is 80, and the stop on line 3 is the throw. The next stop is line 11, in the catch block; line 9 never runs. The run prints failed: undefined. After the edit, nothing runs, and the compiler reports Line 11: 'problem' is of type 'unknown'.

Expected output

failed: undefined

03 / Narrowing

How do you find out what was thrown?

The checks from TypeScript narrowing and discriminated unions work on a caught value. problem instanceof Error is true for an Error and for every subclass of Error, such as TypeError. After it, the compiler lets you read the fields of an Error:

  • message: the text given to the constructor.
  • name: the kind, such as "Error" or "TypeError".
  • stack: the stack trace as text.
  • cause: the earlier failure kept inside, as in Propagation and unwinding.

typeof problem === "string" catches a thrown text. Anything else needs a fallback.

Write these checks once, in a function such as reasonOf, and call it from every catch block. cause has the type unknown too, because any value can be stored there, so reasonOf checks it again in the same way. The experiment below does that.

main.ts
function reasonOf(problem: unknown): string {
  if (problem instanceof Error) {
    return problem.message;
  }
  if (typeof problem === "string") {
    return problem;
  }
  return "unknown problem";
}

Test a caught value with instanceof Error, then typeof, and keep a fallback for every other value.

Precisely: where instanceof fails

instanceof follows the object’s prototype chain while the program runs. An Error made in another realm, such as an iframe or Node’s vm module, comes from a different Error class, and instanceof Error is false for it. Code that receives such values tests the shape instead: typeof problem === "object" && problem !== null && "message" in problem. stack has the type string | undefined, because the language does not require it.

Experiment

Narrow a caught Error and its cause

instanceof Error narrows problem to Error, so message and cause compile. cause has the type unknown, because any value can be stored there, so reasonOf checks it again in the same way, by calling itself. The fallback catches every value that is neither an Error nor a text.

Why it matters One helper that tests instanceof Error, then typeof "string", then falls back, can read any caught value, including a cause.

The Error has a cause that is a text. Predict the printed line, and how many reasonOf frames appear at once. Then change cause: "card declined" on line 15 to cause: 51 and predict again.

function reasonOf(problem: unknown): string {
  if (problem instanceof Error) {
    if (problem.cause !== undefined) {
      return problem.message + " (" + reasonOf(problem.cause) + ")";
    }
    return problem.message;
  }
  if (typeof problem === "string") {
    return problem;
  }
  return "unknown problem";
}

try {
  throw new Error("sale failed", { cause: "card declined" });
} catch (problem) {
  console.log(reasonOf(problem));
}
What to look for

The stop on line 15 is followed by a stop on line 17, in the catch block. In the first reasonOf frame, problem shows Error: sale failed {cause: "card declined"}: an Error, with its cause in braces. The stepper goes from line 2 to line 3 and line 4: instanceof Error was true, and a cause exists. On line 4 reasonOf calls itself, and the frame list shows two reasonOf frames. In the newer one, problem shows "card declined", and the stops are line 2, line 8, and line 9: the text branch. Each frame of reasonOf also lists reasonOf itself, the function’s own name. The run prints sale failed (card declined). After the edit, the newer frame shows problem 51, the stops go from line 8 to line 11, and the run prints sale failed (unknown problem).

Expected output

sale failed (card declined)

04 / Error classes

What does your own error class add?

A class that extends Error, as in Dispatch, inheritance, and composition, can carry details as fields. Below, SoldOut keeps the fruit. problem instanceof SoldOut narrows problem to SoldOut, so problem.fruit compiles without as.

One field is easy to forget: name. The class does not set it, so a SoldOut object uses the name of Error, which is "Error". String(problem) then gives Error: mango sold out, and a log cannot tell your error from any other. Set this.name in the constructor. Recognize the class with instanceof, not by its name: a name is text, and any class can use the same text.

A catch block should handle only what it recognizes. throw problem passes every other value on, unchanged, as Errors as values or exceptions did for errors that were not its own.

main.ts
class SoldOut extends Error {
  fruit: string;
  constructor(fruit: string) {
    super(fruit + " sold out");
    this.fruit = fruit;
    this.name = "SoldOut";
  }
}

An Error subclass carries details in fields and keeps the name "Error" until its constructor sets this.name; test it with instanceof.

Coming from Java: A Java exception class prints its own class name; a JavaScript Error subclass prints Error until you set this.name.

Precisely: older compile targets

When TypeScript compiles to ES5, a class that extends Error does not get its own prototype, so instanceof SoldOut is false and its methods are missing. The fix there is Object.setPrototypeOf(this, SoldOut.prototype) in the constructor. The stepper compiles to ES2022, where classes are native and the problem does not occur. When several classes are tested, test a subclass before Error, because instanceof Error is true for both.

Experiment

Print an error made by your own class

SoldOut objects get message from super(…) and fruit from the constructor. name is not set by the class, so every SoldOut object uses the name of Error, which is "Error". instanceof SoldOut tests the class itself while the program runs, and narrows problem so that problem.fruit compiles.

Why it matters Give your error class a name in its constructor, so logs say what it is. Recognize it with instanceof, which also gives the compiler its fields.

Predict the two printed lines, and which name String(problem) shows. Then add the line this.name = "SoldOut"; after line 5, inside the constructor, and predict again.

class SoldOut extends Error {
  fruit: string;
  constructor(fruit: string) {
    super(fruit + " sold out");
    this.fruit = fruit;
  }
}

function order(fruit: string): number {
  if (fruit === "mango") {
    throw new SoldOut(fruit);
  }
  return 3;
}

try {
  order("mango");
} catch (problem) {
  if (problem instanceof SoldOut) {
    console.log(String(problem));
    console.log("offer something other than " + problem.fruit);
  } else {
    throw problem;
  }
}
What to look for

Step into order and then into the SoldOut constructor. Between the stop on line 4 (super) and the stop on line 5, the frame list shows a frame named <instance_members_initializer>: that is where JavaScript sets up the class’s fields, and it is normal. The new object is listed first in the frame, as this: Error: mango sold out {fruit: undefined} before line 5 runs, and {fruit: "mango"} after it. Back at the top level, the stepper goes from line 19 to line 20, so instanceof SoldOut was true. The run prints Error: mango sold out, then offer something other than mango. After the edit, the stepper also stops on the new line 6, and the first line becomes SoldOut: mango sold out.

Expected output

Error: mango sold out
offer something other than mango

05 / Promises

Where does a rejected promise go?

Some work finishes later, such as a request to a server. A Promise is an object that stands for such a result. Later it is fulfilled with a value, or rejected with a reason.

A function written as async function always returns a Promise. A throw inside it does not throw at the caller. It rejects the Promise, and the thrown value becomes the reason. Inside another async function, await waits for a Promise. It gives the value when the Promise is fulfilled, and it throws the reason when the Promise is rejected. So a try around an await catches a rejection, and the caught value is unknown, as in every catch.

Waiting changes the order. At its first await, an async function hands a Promise to its caller, and the caller goes on with its next line. The catch block runs later.

Without await, you pass functions to the Promise: promise.then(onValue, onReason) calls one of the two, and promise.catch(onReason) handles only a rejection. Here the reason has the type any, not unknown, because useUnknownInCatchVariables covers only catch blocks. Write the type yourself: .catch((reason: unknown) => …).

main.ts
async function report(crate: number): Promise<void> {
  try {
    const kg = await weigh(crate);
    console.log(kg + " kg");
  } catch (problem) {
    // a rejection of weigh's Promise arrives here
    console.log(String(problem));
  }
}

A throw in an async function rejects its Promise; await inside a try turns the rejection back into a throw that the catch receives.

Coming from Java: A CompletableFuture wraps a failure in an ExecutionException when you call get(); await throws the original reason itself.

Precisely: rejections nobody handles

Since Node.js 15, a rejected Promise that no code handles stops the program with the reason, like an uncaught throw. A file that is a module can use await at the top level; the stepper runs each file as CommonJS, so its experiments call an async function instead.

Experiment

Catch a rejection at an await

An async function runs until its first await, and then returns a Promise to its caller. The caller goes on with its next line. When the Promise from weigh is settled, report continues at the await: a rejected Promise throws its reason there, inside the try, and the catch receives it. A fulfilled Promise gives its value, and report goes on in the same way.

Why it matters With await inside a try, a rejection reaches the catch, but later than the caller’s next line. The caught value is unknown, as in every catch.

weigh throws for crate 3. Predict the order of the two printed lines. Then change report(3) on line 17 to report(1) and predict the order again.

async function weigh(crate: number): Promise<number> {
  if (crate > 2) {
    throw new Error("no crate " + crate);
  }
  return 10 * crate;
}

async function report(crate: number): Promise<void> {
  try {
    const kg = await weigh(crate);
    console.log("crate " + crate + ": " + kg + " kg");
  } catch (problem) {
    console.log("crate " + crate + ": " + String(problem));
  }
}

report(3);
console.log("asked for a crate");
What to look for

Step into report and into weigh. The frame list shows weigh on top of report, on top of <top level>, and kg shows not initialized yet. After the throw on line 3, the next stop is line 18 at the top level: report and weigh have left the frame list, although report has not finished. The run prints asked for a crate. The next stop is line 13, in report’s catch block, and the frame list now shows report alone: the top level has finished, and report continued later, where it was waiting at the await. The run then prints crate 3: Error: no crate 3. After the edit, weigh returns 10, the later stop is line 11 instead of line 13, and the run prints asked for a crate, then crate 1: 10 kg.

Expected output

asked for a crate
crate 3: Error: no crate 3

06 / In production

What goes wrong with thrown values in real programs?

Each bug below passes the compiler, because the caught value was read without a check, or never reached the catch at all.

An Error logged as {}. JSON.stringify(problem) writes only the fields that a loop over the object’s keys would see, and an Error keeps message and stack in hidden fields. The log line is {}. The stepper’s panel shows an Error the same way. Log String(problem), or problem.stack for the list of calls.

A reason of type any. promise.catch((reason) => show(reason.message)) compiles, because the reason in .then and .catch callbacks is any. When the Promise is rejected with a text, the screen shows undefined. Write (reason: unknown) and use the same checks as in a catch block.

A return without await inside a try. In an async function, return save(kg) inside a try hands the Promise to the caller before it is settled, so its rejection goes past the catch. Write return await save(kg). The experiment shows both.

Throwing a text in your own code. throw "sold out" has no stack trace, and no instanceof test can recognize it. Throw an Error, or an object of your own Error class.

main.ts
async function record(kg: number): Promise<string> {
  try {
    // await keeps a rejection inside this try
    return await save(kg);
  } catch (problem) {
    return "not saved";
  }
}

Log an Error with String or stack, give a callback’s reason the type unknown, write return await inside a try, and throw only Errors.

Experiment

Return a promise from inside a try

return save(kg) hands the Promise from save to the caller of record without waiting for it inside the try. When the Promise is rejected later, record has already left its try, so the rejection goes past the catch. return await save(kg) waits inside the try, so the rejection is thrown there and caught.

Why it matters Inside a try, write return await, not return, when the catch should handle a rejection.

save rejects for 0 kg, and record has a catch around the call. Predict which callback on line 17 or line 18 prints. Then change line 10 to return await save(kg); and predict again.

async function save(kg: number): Promise<string> {
  if (kg <= 0) {
    throw new Error("no weight");
  }
  return "saved " + kg + " kg";
}

async function record(kg: number): Promise<string> {
  try {
    return save(kg);
  } catch (problem) {
    return "not saved";
  }
}

record(0).then(
  (text) => console.log(text),
  (problem) => console.log("escaped: " + String(problem)),
);
What to look for

Step into record and into save. After the throw on line 3, the stepper goes back to line 10 in record, then to the top level, and then into the frame arrow function (line 18): the second callback. Its problem shows Error: no weight. The stepper never stops on line 12. The run prints escaped: Error: no weight. After the edit, the stop after the top level is line 12, in record’s catch block, then the first callback on line 17 shows text "not saved", and the run prints not saved.

Expected output

escaped: Error: no weight

07 / Try it

Can you predict what the basket prints?

pick is async: it throws an object of a class, or a text, or returns the fruit. Follow basket through each fruit, and remember when basket hands control back to the top level.

main.ts
class Bruised extends Error {
  constructor(fruit: string) {
    super(fruit + " is bruised");
    this.name = "Bruised";
  }
}

async function pick(fruit: string): Promise<string> {
  if (fruit === "peach") {
    throw new Bruised(fruit);
  }
  if (fruit === "fig") {
    throw "no figs today";
  }
  return fruit;
}

async function basket(fruits: string[]): Promise<void> {
  for (const fruit of fruits) {
    try {
      console.log("got " + (await pick(fruit)));
    } catch (problem) {
      if (problem instanceof Bruised) {
        console.log(problem.name + ": " + problem.message);
      } else {
        console.log("other: " + String(problem));
      }
    }
  }
}

basket(["plum", "peach", "fig"]);
console.log("basket started");

What are the four output lines, in order?

Reveal the reasoning

basket started, got plum, Bruised: peach is bruised, other: no figs today. basket runs until its first await, even though pick("plum") succeeds, and then hands a Promise back to the top level, which prints basket started. Then basket continues: the plum is fulfilled. The peach rejects with a Bruised object, which instanceof Bruised recognizes, and its name was set in the constructor. The fig rejects with a text, which is not a Bruised, so String gives the text itself.

Practice tickets

  1. TS-501 · The waffle stand screen says payment failed: undefined → Free
  2. TS-502 · Honey orders larger than the stock fail without an offer → Sign in · Pro
  3. TS-503 · Tofu delivery requests crash when every courier is busy → Sign in · Pro

Keep exploring

I/O, buffering, and external state →

Primary references