cf.completefrontend
Open lab My progress
THE TYPESCRIPT FIELD GUIDE

Typing errors

See why catch gives you unknown, narrow errors safely, give custom error classes typed fields, and return Result types that make callers handle failure.

By the end, you can
  • 01
    Explain unknown in catchSay why a caught error is unknown, which flag does it, and why .catch callbacks still get any.
  • 02
    Narrow errors safelyTurn any thrown value into a message, and read typed fields from your own error classes.
  • 03
    Return failures as valuesWrite a Result<T, E> type so the compiler makes every caller handle the failure.

Anything can be thrown

A checkout page charges a card, and the charge can fail. In JavaScript, throw can throw any value, not only Error objects (the try, catch & finally lesson covers both). This card code throws plain text, and the catch block expects an Error. First, look at it the way TypeScript saw it before version 4.4. The example turns off the flag useUnknownInCatchVariables, which the next section explains:

A caught error typed anyPop out in the code editor (opens in a new tab)TypeScript
function payWithCard(amount: number) {  if (amount > 100) throw "Card declined";  return `Paid $${amount}`;} try {  console.log(payWithCard(120));} catch (error) {  //     ^?  console.log(`Payment failed: ${error.message}`);}

Line 2 throws the text "Card declined" when the amount is over 100. Line 7 pays 120, so it throws, and line 8 catches the text as error. The answer after the ^? on line 9 shows any, the type that turns checking off. So line 10 reads error.message with no complaint, and prints Payment failed: undefined: text has no message.

Now the same code with this site's normal settings, where strict is on:

The same error as unknown (an error on purpose)Pop out in the code editor (opens in a new tab)TypeScript
function payWithCard(amount: number) {  if (amount > 100) throw "Card declined";  return `Paid $${amount}`;} try {  console.log(payWithCard(120));} catch (error) {  //     ^?  console.log(`Payment failed: ${error.message}`);}

The answer on line 9 is now unknown, and line 10 gets an error, on purpose: 'error' is of type 'unknown'. In plain words: "this could be anything, so check what it is before you read from it". The code still prints Payment failed: undefined if you run it. The error is TypeScript pointing at the bug before a customer sees it.

Three cards on the left: new Error(…), "Card declined", and { code: 402 }. Arrows from all three lead into a box named error, which holds a card marked unknown. Under the box: catch (error).new Error(…)"Card declined"{ code: 402 }unknownerrorcatch (error)
throw can send any value: an Error, text, or a plain object. They all land in the same catch (error), so the only honest type for error is unknown.
Real-life analogyA lost property office

At a station's lost property office, the desk takes whatever is handed in. Nobody assumes a found item is a phone and tries to call from it. They look at it first, then decide what to do.

In real life: People hand in anything: a phone, an umbrella, a note
In TypeScript: throw can throw any value: an Error, text, an object
In real life: The desk logs it as "item, not yet identified"
In TypeScript: The catch variable is unknown
In real life: Staff check it's a phone before looking for its owner's number
In TypeScript: instanceof Error comes before reading message
In real life: Some items arrive in a labelled bag with the owner's details
In TypeScript: Your own error classes carry typed fields, like status
The main idea

A caught error is unknown, because any value can be thrown. Check what it is, usually with instanceof Error, before you read from it. For failures callers should expect, return them as values with a Result type, so TypeScript makes callers handle them.

Why catch gives you unknown

TypeScript can't know what a try block might throw. A function's type says what it gives back, but nothing about what it throws, and any line can call code that throws text. So since version 4.4, the flag useUnknownInCatchVariables makes the catch variable unknown. strict turns it on, along with the other strict checks. (unknown is the type for "a value you haven't checked yet"; the any, unknown & never lesson covers it.)

You may write the type on a catch variable, but only unknown or any. This mistake is on purpose:

A catch variable can't be typed Error (an error on purpose)Pop out in the code editor (opens in a new tab)TypeScript
try {  JSON.parse("{ broken");} catch (error: Error) {  console.log(error.name);} try {  JSON.parse("{ broken");} catch (error: unknown) {  console.log(error instanceof SyntaxError);}

Line 2 parses broken JSON, which throws a SyntaxError. Line 3 says the error is an Error, and TypeScript refuses: Catch clause variable type annotation must be 'any' or 'unknown' if specified. In plain words: "I can't promise what was thrown, so I won't let you say it". Line 9 writes unknown, which is allowed and changes nothing. Line 10 checks with instanceof. When it runs, line 4 prints SyntaxError and line 10 prints true.

Promises are different. A promise that fails is "rejected", and .catch runs a callback with the reason (the Error handling with promises lesson covers this). TypeScript's own types give that reason the type any, and strict doesn't change it:

A .catch callback still gets anyPop out in the code editor (opens in a new tab)TypeScript
async function payWithCard(amount: number): Promise<string> {  if (amount > 100) throw "Card declined";  return `Paid $${amount}`;} payWithCard(120).catch((error) => {  //                    ^?  console.log(`Payment failed: ${error.message}`);}); payWithCard(120).catch((error: unknown) => {  console.log(`Payment failed: ${String(error)}`);});

Line 2 throws text again, so the promise from line 6 is rejected. The answer on line 7 shows error is any, so line 8 reads message with no complaint and prints Payment failed: undefined. Line 11 writes (error: unknown) itself, which TypeScript allows, and now it would ask for a check. Line 12 uses String, which works on any value, and prints Payment failed: Card declined. A linter rule such as typescript-eslint's use-unknown-in-catch-callback-variable asks for this on every .catch.

The table below lists every place a failure reaches your code. Read each row from left to right:

The type of a caught error
Where the error arrivesIts type with strict on
catch (error)unknown. You check it before you use it.
catch (error: unknown)unknown, written out. The same as above, and clear to readers.
catch (error: any)any. Checking is off for this error.
catch (error: Error)Not allowed. TypeScript can't promise that what was thrown is an Error.
.catch((error) => …) on a promiseany, even with strict on. Write (error: unknown) yourself.
reason in a rejected Promise.allSettled resultany, for the same reason.
catch (error) with useUnknownInCatchVariables: falseany, as in TypeScript before version 4.4.

Narrowing an unknown error

To use an unknown error, narrow it: check what it is, so TypeScript knows more inside the check (the in & instanceof narrowing lesson covers instanceof). Try four ways to write line 9, and watch what TypeScript says and what prints:

Playground: read a message from an unknown error
A payment that throws textPop out in the code editor (opens in a new tab)TypeScript
function payWithCard(amount: number) {  if (amount > 100) throw "Card declined";  return `Paid $${amount}`;} try {  console.log(payWithCard(120));} catch (error) {  const message = error.message;  console.log(`Payment failed: ${message}`);}
Console outputPayment failed: undefined
What happens
TypeScript1 error: line 9
Messagelost
Try it yourself

TypeScript stops you: 'error' is of type 'unknown'. It's right to. The card throws text, and text has no message, so this prints Payment failed: undefined.

TypeScript checks the code for real for each choice, and the output is what the code prints. The card throws the text "Card declined". Each button changes only line 9. Reset goes back to error.message.

Only two choices print the real reason. error as Error is a type assertion (the Type assertions lesson covers them): it removes the error by making a promise you can't keep, because the value is text. instanceof Error really checks, and String(error) works on any value. Most code does both, in one small function:

Playground: throw something else
getErrorMessage meets a thrown valuePop out in the code editor (opens in a new tab)TypeScript
function getErrorMessage(error: unknown): string {  if (error instanceof Error) return error.message;  if (typeof error === "string") return error;  return "Something went wrong";} try {  throw new Error("Card declined");} catch (error) {  console.log(getErrorMessage(error));}
Console outputCard declined
What happens
Thrownan Error
TypeScriptNo errors
Try it yourself

An Error passes the check on line 2, so the function gives back its message.

The output is what this code prints for each thrown value. TypeScript has no errors for any of them: getErrorMessage takes unknown, so it handles them all. Each button changes only line 8. Reset goes back to an Error.

Line 1 takes unknown, so you can pass it any caught value without a check. Line 2 returns the message of a real Error, including every class that extends it, like TypeError. Line 3 returns text as it is. Line 4 covers everything else with a message you chose. The function always gives back a string, so the rest of the page never has to think about what was thrown.

A card marked unknown on the left, with three arrows to three cards. error instanceof Error leads to Error, which gives error.message. typeof error === "string" leads to string, which gives error. Anything else leads to unknown, which gives "Something went wrong".unknownerror instanceof ErrorErrorerror.messagetypeof error === "string"stringerroranything elseunknown"Something went wrong"
getErrorMessage asks one question at a time. Each check narrows error to a type you can use, and anything left over still gets a message.

Custom error classes

Sometimes a page needs more than a message, like the HTTP status of a failed request: 401 means "log in first", and 404 means "not found". A class that extends Error can carry it (the Throwing & custom errors lesson builds these in JavaScript, and Classes in TypeScript covers typing class fields):

An error class with a typed statusPop out in the code editor (opens in a new tab)TypeScript
class HttpError extends Error {  status: number;   constructor(status: number, message: string) {    super(message);    this.name = "HttpError";    this.status = status;  }} async function loadCart(): Promise<string[]> {  throw new HttpError(401, "Not logged in");} async function showCart() {  try {    console.log(await loadCart());  } catch (error) {    if (error instanceof HttpError) {      console.log(error.status === 401 ? "Please log in" : `Error ${error.status}`);      //          ^?    } else {      console.log("Could not load the cart");    }  }} showCart();

Line 1 makes HttpError, a kind of Error. Line 2 declares a status field that holds a number. Line 5 passes the message to Error with super. Line 6 sets name, so logs and stack traces say HttpError instead of Error. Line 7 stores the status. Line 12 is a stand-in for a server that answers 401.

Line 18 catches an unknown, and line 19 checks error instanceof HttpError. Inside that check, the answer on line 21 shows error is an HttpError, so line 20 can read error.status as a number. It prints Please log in.

An error can also carry the error that caused it. new Error(message, { cause }) wraps a low-level failure in a clearer one, and keeps the original:

An error that keeps its causePop out in the code editor (opens in a new tab)TypeScript
async function sendOrder(): Promise<number> {  throw new Error("Network down");} async function checkout() {  try {    return await sendOrder();  } catch (error) {    throw new Error("Checkout failed", { cause: error });  }} checkout().catch((error: unknown) => {  if (!(error instanceof Error)) return;  console.log(error.message);  const cause = error.cause;  //    ^?  if (cause instanceof Error) console.log(`Because: ${cause.message}`);});

Line 2 fails with Network down. Line 9 catches it and throws a new error with the first one as its cause. Line 13 catches the new error as unknown, and line 14 stops unless it's an Error. The answer on line 17 shows the cause is unknown too: any value can be passed as a cause. So line 18 checks it before reading its message. It prints Checkout failed, then Because: Network down.

Result types instead of throwing

A thrown error is invisible in a function's type. For failures you expect, like a customer typing "abc" as a quantity, you can return the failure as a value instead. A Result type is a union of a success and a failure, told apart by an ok field (the Discriminated unions lesson covers this pattern, and the Error handling strategies lesson compares it with throwing). This first use forgets to check, on purpose:

Using a Result without checking it (an error on purpose)Pop out in the code editor (opens in a new tab)TypeScript
type Result<T, E> = { ok: true; value: T } | { ok: false; error: E }; function parseQuantity(text: string): Result<number, string> {  const quantity = Number(text);  if (Number.isInteger(quantity) && quantity > 0) return { ok: true, value: quantity };  return { ok: false, error: `"${text}" is not a quantity` };} const result = parseQuantity("abc");console.log(`Total: $${result.value * 4}`);

Line 1 defines Result once, with two type parameters: T for the value and E for the error (the Generic types & interfaces lesson covers them). Line 3 says parseQuantity gives back a number or a text error. Line 5 returns a success for a whole number above zero, and line 6 returns a failure for anything else. Line 10 reads result.value without checking, and TypeScript says Property 'value' does not exist on type 'Result<number, string>': a failure has no value. Run it, and it prints Total: $NaN.

Check ok, then use the value or the errorPop out in the code editor (opens in a new tab)TypeScript
type Result<T, E> = { ok: true; value: T } | { ok: false; error: E }; function parseQuantity(text: string): Result<number, string> {  const quantity = Number(text);  if (Number.isInteger(quantity) && quantity > 0) return { ok: true, value: quantity };  return { ok: false, error: `"${text}" is not a quantity` };} const result = parseQuantity("abc");if (result.ok) {  console.log(`Total: $${result.value * 4}`);} else {  console.log(result.error);  //                 ^?}

Line 10 checks result.ok. Inside, result is the success, so line 11 can use value. In the else, it's the failure, and the answer on line 14 shows error is a string. It prints "abc" is not a quantity. With the text "3", it would print Total: $12.

Left, throwing: a card parseQuantity(): number leads to a card a number, and a note says a failure is thrown, not in the type. Right, returning a Result: a card parseQuantity(): Result leads to two cards, ok: true, value and ok: false, error, and a note says both ways are in the type.throwingparseQuantity(): numbera numbera failure is thrown, not in the typereturning a ResultparseQuantity(): Resultok: true, valueok: false, errorboth ways are in the type
A function that throws only names its success in its type, so callers can forget the failure. A Result names both ways, and TypeScript makes callers check ok before they use value.

The error type can be a union of codes, one for each failure the caller should handle (the Literal types lesson covers these), and it works the same in async code:

An async Result with error codesPop out in the code editor (opens in a new tab)TypeScript
type Result<T, E> = { ok: true; value: T } | { ok: false; error: E };type Cart = { items: number; total: number };type CartError = "not-logged-in" | "server-down"; async function loadCart(loggedIn: boolean): Promise<Result<Cart, CartError>> {  if (!loggedIn) return { ok: false, error: "not-logged-in" };  return { ok: true, value: { items: 3, total: 12 } };} function errorText(error: CartError): string {  switch (error) {    case "not-logged-in":      return "Please log in";    case "server-down":      return "Try again in a minute";  }} async function showCart(loggedIn: boolean) {  const result = await loadCart(loggedIn);  console.log(result.ok ? `Total: $${result.value.total}` : errorText(result.error));} showCart(true).then(() => showCart(false));

Line 3 lists the two ways loading a cart can fail. Line 5 gives back a Promise of a Result, so the failure is in the type even across await (the Promises & async functions lesson covers Promise<T>). Lines 10 to 17 turn each code into a message with a switch. It has a case for every code, so TypeScript knows every path returns. Line 21 prints the total or the message: Total: $12 for a logged-in visitor, then Please log in. Add a third code to line 3, and TypeScript marks line 10 until errorText handles it (the Exhaustiveness checking lesson covers this); exercise 5 tries it.

The table below compares the two ways to fail. Read each row from left to right:

Throwing compared with returning a Result
QuestionThrowing an errorReturning a Result
Does the function's type say it can fail?No. Its return type only names the value it gives back on success.Yes. Result<number, string> names the failure too.
Does TypeScript make callers handle failure?No. Leaving out try and catch is allowed.Yes. Callers can't read value until they check ok.
What does a caller get on failure?Whatever was thrown, typed unknown.An error with the type you chose, like string or a union of codes.
What is it good for?Bugs, and failures nothing nearby can fix, which should stop the work.Failures callers should expect, like bad input or a declined card.

Handling a failed checkout

A real checkout uses both. The request code throws, as most libraries do. The page code catches once, turns each failure into a message, and returns a Result that the page has to check:

Turn thrown errors into a ResultPop out in the code editor (opens in a new tab)TypeScript
class HttpError extends Error {  status: number;   constructor(status: number, message: string) {    super(message);    this.name = "HttpError";    this.status = status;  }} type Result<T, E> = { ok: true; value: T } | { ok: false; error: E }; async function sendOrder(total: number): Promise<number> {  if (total > 100) throw new HttpError(402, "Payment required");  if (total < 0) throw "Bad total";  return 7;} async function placeOrder(total: number): Promise<Result<number, string>> {  try {    return { ok: true, value: await sendOrder(total) };  } catch (error) {    if (error instanceof HttpError && error.status === 402) {      return { ok: false, error: "Your card was declined" };    }    return { ok: false, error: "Something went wrong" };  }} async function showOrders() {  for (const total of [12, 120, -1]) {    const result = await placeOrder(total);    console.log(result.ok ? `Order ${result.value} placed` : result.error);  }} showOrders();

Lines 1 to 9 are HttpError from before, and line 11 is Result. Lines 13 to 17 are a stand-in for the server: over 100 throws an HttpError with status 402, a total below zero throws plain text, and anything else gives back order number 7. Line 19 says placeOrder gives back a promise of a Result, never a thrown error. Line 21 tries the order. Line 23 turns a 402 into a clear message, and line 26 covers everything else, including the text.

Line 33 has to check result.ok before reading value. The code prints Order 7 placed, Your card was declined, and Something went wrong. Now sort some one-line snippets by what TypeScript says about them:

Error or fine?
  • try {} catch (error) { console.log(error.message); }
  • try {} catch (error: Error) {}
  • try {} catch (error: unknown) { console.log(error.message); }
  • try {} catch (error: any) { console.log(error.message); }
  • try {} catch (error) { if (error instanceof Error) console.log(error.message); }
  • Promise.reject("Down").catch((error) => console.log(error.message));
  • const cause = new Error("Failed", { cause: 404 }).cause; console.log(cause.toFixed());
  • try {} catch (error) { if (typeof error === "string") console.log(error.toUpperCase()); }
Try it yourself
0 of 8 correct

Sort each line by what TypeScript says about it. Each line is checked on its own, with strict on.

Choose a category for every card. You can change an answer at any time; Reset clears them all.

Common mix-ups

instanceof checks run in order, and TypeScript doesn't tell you when one hides another. This code has no type errors, and still takes the wrong branch:

A general check before a specific onePop out in the code editor (opens in a new tab)TypeScript
class HttpError extends Error {  status = 404;} function report(error: unknown) {  if (error instanceof Error) {    console.log(`Something went wrong: ${error.message}`);  } else if (error instanceof HttpError) {    console.log(`Status ${error.status}`);  }} report(new HttpError("Not found"));

Line 1 makes HttpError with a status of 404. Line 6 checks for any Error first, and an HttpError is an Error, so line 7 prints Something went wrong: Not found. Line 9 can never run. Check the most specific class first, as in & instanceof narrowing showed.

  • "A caught error is always an Error." No. Any value can be thrown: text, objects, even undefined. That's why it's unknown.
  • "catch (error: Error) tells TypeScript what was thrown." No. TypeScript only allows any or unknown there. Narrow inside the block.
  • "error as Error is the same as checking." No. as checks nothing. Text thrown as an error still has no message.
  • "strict makes .catch callbacks unknown too." No. A .catch callback's value is still any. Write (error: unknown) yourself.
  • "A function's type says what it can throw." No. TypeScript has no type for thrown errors. Use a Result when callers must handle a failure.
  • "Results replace throwing everywhere." No. Throw for bugs and failures nothing nearby can fix; return a Result for failures callers should expect.

Practice exercises

Each exercise is TypeScript. Press Edit & run on the starter code to try a fix: the errors update as you type, and the console shows what it prints. Try a guess first, then use the hints.

Exercise 1 · Warm-upRead the type of a caught error

What type does error get? Guess first. Then point at error to check, or press Edit & run and add a // ^? line under it.

Starter codePop out in the code editor (opens in a new tab)TypeScript
try {  JSON.parse("{ broken");} catch (error) {  console.log("Could not read the saved cart");}

Answer, then press Check. Spacing and letter case don’t matter.

    Exercise 2 · Warm-upFind the line TypeScript marks

    This code means well. Which line will TypeScript mark?

    Starter codePop out in the code editor (opens in a new tab)TypeScript
    function saveCart(items: number) {
      if (items > 50) throw new RangeError("Too many items");
    }
    
    try {
      saveCart(60);
    } catch (error: RangeError) {
      console.log(error.message);
    }

    Answer, then press Check. Spacing and letter case don’t matter.

      Exercise 3 · PracticeGet the real message

      TypeScript marks line 9, and the code prints Coupon failed: undefined. Fix line 9 without as. What does your fixed code print?

      Starter codePop out in the code editor (opens in a new tab)TypeScript
      function applyCoupon(code: string) {  if (code !== "TEA10") throw `Unknown coupon ${code}`;  return 10;} try {  applyCoupon("CAKE5");} catch (error) {  console.log(`Coupon failed: ${error.message}`);}

      Answer, then press Check. Spacing and letter case don’t matter.

        Exercise 4 · PracticeGive the error a status

        Make HttpError carry a status, and throw one with status 404. What does your fixed code print?

        Starter codePop out in the code editor (opens in a new tab)TypeScript
        class HttpError extends Error {  constructor(message: string) {    super(message);    this.name = "HttpError";  }} try {  throw new HttpError("Not found");} catch (error) {  if (error instanceof HttpError) console.log(`Status ${error.status}`);}

        Answer, then press Check. Spacing and letter case don’t matter.

          Exercise 5 · ChallengeHandle a new failure

          Someone added "out-of-stock" to CartError, and TypeScript marks line 3. Fix errorText. What does your fixed code print?

          Starter codePop out in the code editor (opens in a new tab)TypeScript
          type CartError = "not-logged-in" | "server-down" | "out-of-stock"; function errorText(error: CartError): string {  switch (error) {    case "not-logged-in":      return "Please log in";    case "server-down":      return "Try again in a minute";  }} console.log(errorText("out-of-stock"));

          Answer, then press Check. Spacing and letter case don’t matter.

            Exercise 6 · ChallengeRead the cause safely

            The code runs fine today, but TypeScript marks line 14. Make it safe for any cause. What does your fixed code print?

            Starter codePop out in the code editor (opens in a new tab)TypeScript
            async function sendOrder(): Promise<number> {  throw new Error("Network down");} async function checkout() {  try {    return await sendOrder();  } catch (error) {    throw new Error("Checkout failed", { cause: error });  }} checkout().catch((error: unknown) => {  if (error instanceof Error) console.log(`${error.message}, because: ${error.cause.message}`);});

            Answer, then press Check. Spacing and letter case don’t matter.

              Check your understanding

              For each question, ask: what could have been thrown, and what does TypeScript know about it? The quiz shows its code without the checker, so the error list doesn't give the answers away.

              The Typing errors quiz · 7 questionsScore: first tries count
              1. Question 1 of 7With strict on, what type does error get in catch (error)?

                Choose an answer to see the explanation.

              2. Question 2 of 7What does TypeScript say about line 7?

                Read the code, then predictPop out in the code editor (opens in a new tab)TypeScript
                function checkCoupon(code: string) {
                  if (code !== "TEA10") throw new Error("Unknown coupon");
                }
                
                try {
                  checkCoupon("CAKE5");
                } catch (error: Error) {
                  console.log(error.message);
                }

                Choose an answer to see the explanation.

              3. Question 3 of 7What type does error get in this .catch callback?

                Read the code, then predictPop out in the code editor (opens in a new tab)TypeScript
                async function loadCart(): Promise<string[]> {
                  throw new Error("Server down");
                }
                
                loadCart().catch((error) => console.log(error.message));

                Choose an answer to see the explanation.

              4. Question 4 of 7What does this code print when a plain object is thrown?

                Read the code, then predictPop out in the code editor (opens in a new tab)TypeScript
                function getErrorMessage(error: unknown): string {
                  if (error instanceof Error) return error.message;
                  if (typeof error === "string") return error;
                  return "Something went wrong";
                }
                
                try {
                  throw { code: 402 };
                } catch (error) {
                  console.log(getErrorMessage(error));
                }

                Choose an answer to see the explanation.

              5. Question 5 of 7What does report print, and does TypeScript warn about the order of the checks?

                Read the code, then predictPop out in the code editor (opens in a new tab)TypeScript
                class HttpError extends Error {
                  status = 404;
                }
                
                function report(error: unknown) {
                  if (error instanceof Error) {
                    console.log(`Something went wrong: ${error.message}`);
                  } else if (error instanceof HttpError) {
                    console.log(`Status ${error.status}`);
                  }
                }
                
                report(new HttpError("Not found"));

                Choose an answer to see the explanation.

              6. Question 6 of 7Why does TypeScript mark line 10?

                Read the code, then predictPop out in the code editor (opens in a new tab)TypeScript
                type Result<T, E> = { ok: true; value: T } | { ok: false; error: E };
                
                function parseQuantity(text: string): Result<number, string> {
                  const quantity = Number(text);
                  if (Number.isInteger(quantity) && quantity > 0) return { ok: true, value: quantity };
                  return { ok: false, error: `"${text}" is not a quantity` };
                }
                
                const result = parseQuantity("abc");
                console.log(`Total: $${result.value * 4}`);

                Choose an answer to see the explanation.

              7. Question 7 of 7When is returning a Result a better fit than throwing?

                Choose an answer to see the explanation.

              Key takeaways

              • Any value can be thrown, so with strict on, a catch variable is unknown.
              • A catch variable can only be typed any or unknown, and a .catch callback's value is any unless you write unknown.
              • Narrow with instanceof Error, and fall back to String(error) or a message you chose. Never use as Error as a check.
              • Custom error classes carry typed fields like status. Check the most specific class first.
              • Return a Result<T, E> for failures callers should expect, so TypeScript makes them check ok.

              Remember the one-liner.
              A caught error is unknown until you check it; throw for the unexpected, and return a Result for the expected.

              Coming next: Typing React components. It types the components that show these messages: props, children, and event handlers.

              CompleteFrontend Clear concepts. Working examples.