Skip to content
aviral gupta

// I4.1 · ~38 min · Intermediate

throw, try, catch and finally

After this lesson you can make your functions fail loudly with an Error, handle the errors you expect, and let the others through.

Lesson 1 of 5 in I4 Errors, debugging and testing

Start of the module

You will be able to

  • Throw Error objects with a clear message, and say which code no longer runs after throw
  • Catch errors with try...catch, check their type, and rethrow the ones you cannot handle
  • Predict when finally runs, also after return, and keep return out of finally
  1. Warm-up · Activity 1 of 7

    Warm-up from lesson I3.5: an empty JSON file. What does this print?

    try {
      JSON.parse("");
    } catch (error) {
      console.log(error.name);
    }
  2. Predict · Activity 2 of 7

    Predict before you read on. What does this print?

    try {
      console.log("A");
      throw new Error("B");
      console.log("C");
    } catch (error) {
      console.log(error.message);
    } finally {
      console.log("D");
    }
    console.log("E");
  3. Practice · Activity 3 of 7

    Fill in the operator that checks the type of the caught error.

    if (error ____ RangeError) console.log("range");
    if (error RangeError)
  4. Practice · Activity 4 of 7

    try returns a value. What does this print?

    function load() {
      try {
        return "data";
      } finally {
        console.log("cleanup");
      }
    }
    console.log(load());
  5. Practice · Activity 5 of 7

    parse handles broken JSON and rethrows everything else. What does this print?

    function parse(text) {
      try {
        return JSON.parse(text).items.length;
      } catch (error) {
        if (error instanceof SyntaxError) return 0;
        throw error;
      }
    }
    
    try {
      console.log(parse("{}"));
    } catch (error) {
      console.log("outer:", error.name);
    }
  6. Brain teaser · Activity 6 of 7

    Brain teaser. Both catch and finally return. What does this print?

    function check() {
      try {
        throw new Error("broken");
      } catch (error) {
        return "caught " + error.message;
      } finally {
        return "finally wins";
      }
    }
    console.log(check());
  7. Apply · Activity 7 of 7

    Mini-task: write divide(a, b). It throws a TypeError if a or b is not a number and a RangeError if b is 0; otherwise it returns a / b. Call it for [10, 4], [1, 0] and ["6", 2] in a loop: print the result, or the error’s name and message, and in finally print done with both values.

    Check your work against this list

Build it yourself

Read the worked example, then write the exercises. Your code runs in your browser or on your computer and is never uploaded.

Worked example

Checking input, handling the expected errors

parseAge throws a TypeError for text that is not a whole number and a RangeError for an age out of range. The loop catches each error, so one bad input does not stop the others, and finally reports every input. readSettings handles broken JSON with defaults and rethrows every other error.

main.js

function parseAge(text) {
  const age = Number(text);
  if (!Number.isInteger(age)) throw new TypeError(`Not a whole number: "${text}"`);
  if (age < 0 || age > 150) throw new RangeError(`Out of range: ${age}`);
  return age; // only reached when both checks pass
}

for (const input of ["42", "abc", "-3"]) {
  try {
    console.log("age", parseAge(input));
  } catch (error) {
    console.log(error.name + ": " + error.message);
  } finally {
    console.log("checked", input);
  }
}

function readSettings(text) {
  try {
    return JSON.parse(text);
  } catch (error) {
    if (error instanceof SyntaxError) return {}; // broken JSON: use defaults
    throw error; // anything else is not ours to handle
  }
}
console.log(readSettings('{"theme": "dark"}'), readSettings("{oops"));

Run it with

node main.js

Output

age 42
checked 42
TypeError: Not a whole number: "abc"
checked abc
RangeError: Out of range: -3
checked -3
{ theme: 'dark' } {}
  • The try...catch is inside the loop: one try around the whole loop would stop at the first error.
  • error.name is the type of the error; error.message is the text given to the constructor.
  • readSettings decides by type: only a SyntaxError means broken JSON.
Change it and run it

Tab indents and Shift+Tab outdents. To leave the editor with the keyboard, press Esc, then Tab.

The first run downloads the JavaScript runner (up to 0.1 MB) and keeps it cached. Your code runs in your browser’s own engine and stays on your device.

Exercises

Exercise 1 of 3

A price or an error

parsePrice(text) turns text such as "12.50" into a number. For text that is not a number, or empty, it must throw a TypeError with the message Not a price: "…" (the text in quotes), and for a negative number a RangeError. The starter returns whatever Number gives, so bad input slips through as NaN or 0.

Tab indents and Shift+Tab outdents. To leave the editor with the keyboard, press Esc, then Tab.

The first run downloads the JavaScript runner (up to 0.1 MB) and keeps it cached. Your code runs in your browser’s own engine and stays on your device.

Hints
  1. Hint 1

    Number("abc") is NaN, and Number("") is 0: check both before you return.

  2. Hint 2

    Number.isNaN(price) and text.trim() === "" find the two bad cases.

  3. Hint 3

    throw new TypeError(`Not a price: "${text}"`)

Show a solution

One way to solve it. Yours can look different and still pass the checks.

export function parsePrice(text) {
  const price = Number(text);
  if (text.trim() === "" || Number.isNaN(price)) throw new TypeError(`Not a price: "${text}"`);
  if (price < 0) throw new RangeError(`Negative price: ${price}`);
  return price;
}
Run it on your computer

Install ECMAScript 2026 or newer. Save these files in one folder, open a terminal in that folder, and run the commands below.

main.js

export function parsePrice(text) {
  return Number(text);
}

main.test.js

import {test} from 'node:test';
import assert from 'node:assert/strict';
import {parsePrice} from './main.js';

test('12.50 is 12.5', () => {
  assert.equal(parsePrice('12.50'), 12.5, 'parsePrice("12.50") should be 12.5');
});

test('abc throws a TypeError with the text in the message', () => {
  assert.throws(() => parsePrice('abc'), {name: 'TypeError', message: 'Not a price: "abc"'}, 'parsePrice("abc") should throw TypeError: Not a price: "abc"');
});

test('an empty text throws a TypeError, not 0', () => {
  assert.throws(() => parsePrice(''), {name: 'TypeError'}, 'parsePrice("") should throw a TypeError, but Number("") is 0');
});

test('a negative price throws a RangeError', () => {
  assert.throws(() => parsePrice('-1'), {name: 'RangeError'}, 'parsePrice("-1") should throw a RangeError');
});

package.json

{
  "type": "module"
}

package.json tells Node.js that the .js files are modules; keep it in the folder.

Run the program:

node main.js

Run the checks (needs learnrun.js in the same folder):

node --test
Download learnrun.js

Exercise 2 of 3

Do not swallow every error

loadCart(text) reads a cart saved as JSON: an array of items. Broken JSON should give an empty cart, but valid JSON that is not an array is a bug elsewhere and must throw the TypeError Cart must be an array. The starter’s catch swallows every error, so that TypeError never reaches the caller. Handle only SyntaxError.

Tab indents and Shift+Tab outdents. To leave the editor with the keyboard, press Esc, then Tab.

The first run downloads the JavaScript runner (up to 0.1 MB) and keeps it cached. Your code runs in your browser’s own engine and stays on your device.

Hints
  1. Hint 1

    Give the catch block a name for the error, so you can look at it.

  2. Hint 2

    error instanceof SyntaxError is true only for broken JSON.

  3. Hint 3

    For any other error: throw error;

Show a solution

One way to solve it. Yours can look different and still pass the checks.

export function loadCart(text) {
  try {
    const cart = JSON.parse(text);
    if (!Array.isArray(cart)) throw new TypeError("Cart must be an array");
    return cart;
  } catch (error) {
    if (error instanceof SyntaxError) return [];
    throw error;
  }
}
Run it on your computer

Install ECMAScript 2026 or newer. Save these files in one folder, open a terminal in that folder, and run the commands below.

main.js

export function loadCart(text) {
  try {
    const cart = JSON.parse(text);
    if (!Array.isArray(cart)) throw new TypeError("Cart must be an array");
    return cart;
  } catch {
    return [];
  }
}

main.test.js

import {test} from 'node:test';
import assert from 'node:assert/strict';
import {loadCart} from './main.js';

test('a saved array comes back', () => {
  const cart = loadCart('[{"id": 1}]');
  assert.deepEqual(cart, [{id: 1}], `loadCart gave ${JSON.stringify(cart)}`);
});

test('broken JSON gives an empty cart', () => {
  const cart = loadCart('[{"id": 1');
  assert.deepEqual(cart, [], `loadCart gave ${JSON.stringify(cart)}`);
});

test('an object instead of an array throws the TypeError', () => {
  assert.throws(() => loadCart('{"id": 1}'), {name: 'TypeError', message: 'Cart must be an array'}, 'loadCart should not swallow the TypeError');
});

package.json

{
  "type": "module"
}

package.json tells Node.js that the .js files are modules; keep it in the folder.

Run the program:

node main.js

Run the checks (needs learnrun.js in the same folder):

node --test
Download learnrun.js

Exercise 3 of 3

Always unlock

runLocked(lock, task) sets lock.busy to true, runs task() and returns its result. Afterwards lock.busy must be false again, also when task throws; the error itself must still reach the caller. The starter only unlocks after a successful task, so one failed task leaves the lock busy for ever.

Tab indents and Shift+Tab outdents. To leave the editor with the keyboard, press Esc, then Tab.

The first run downloads the JavaScript runner (up to 0.1 MB) and keeps it cached. Your code runs in your browser’s own engine and stays on your device.

Hints
  1. Hint 1

    Which block runs both after success and after an error?

  2. Hint 2

    You do not need catch here: try...finally lets the error through.

  3. Hint 3

    try { return task(); } finally { lock.busy = false; }

Show a solution

One way to solve it. Yours can look different and still pass the checks.

export function runLocked(lock, task) {
  lock.busy = true;
  try {
    return task();
  } finally {
    lock.busy = false;
  }
}
Run it on your computer

Install ECMAScript 2026 or newer. Save these files in one folder, open a terminal in that folder, and run the commands below.

main.js

export function runLocked(lock, task) {
  lock.busy = true;
  const result = task();
  lock.busy = false;
  return result;
}

main.test.js

import {test} from 'node:test';
import assert from 'node:assert/strict';
import {runLocked} from './main.js';

test('returns the result and unlocks', () => {
  const lock = {busy: false};
  assert.equal(runLocked(lock, () => 42), 42, 'runLocked should return what the task returns');
  assert.equal(lock.busy, false, 'lock.busy should be false after the task');
});

test('the task sees the lock as busy', () => {
  const lock = {busy: false};
  assert.equal(runLocked(lock, () => lock.busy), true, 'lock.busy should be true while the task runs');
});

test('a failing task still unlocks, and its error reaches the caller', () => {
  const lock = {busy: false};
  assert.throws(() => runLocked(lock, () => { throw new Error('disk full'); }), {message: 'disk full'}, 'the error of the task should reach the caller');
  assert.equal(lock.busy, false, 'lock.busy should be false after a failed task');
});

package.json

{
  "type": "module"
}

package.json tells Node.js that the .js files are modules; keep it in the folder.

Run the program:

node main.js

Run the checks (needs learnrun.js in the same folder):

node --test
Download learnrun.js

Common mistakes

Throwing a string

function findUser(id) {
  throw "User " + id + " not found";
}

try {
  findUser(7);
} catch (error) {
  console.log(error.message.toUpperCase());
}

What Node.js prints

TypeError: Cannot read properties of undefined (reading 'toUpperCase')

Why, and the fix

A thrown string has no message, name or stack, so code that expects an Error breaks, and you lose the place where it happened. Throw an Error object: throw new Error(`User ${id} not found`). Use a built-in type, such as TypeError or RangeError, when it fits.

Using a variable from inside try

try {
  const settings = JSON.parse('{"theme": "dark"}');
} catch (error) {
  console.log("broken settings");
}
console.log(settings.theme);

What Node.js prints

ReferenceError: settings is not defined

Why, and the fix

const and let inside the try block exist only in that block. Use the value inside try, or declare it before: let settings = {}; then try { settings = JSON.parse(text); } catch { … }, so it also has a value after an error.

A catch that logs and goes on

let settings;
try {
  settings = JSON.parse("{oops");
} catch (error) {
  console.log("could not read the settings");
}
console.log(settings.theme);

What Node.js prints

TypeError: Cannot read properties of undefined (reading 'theme')

Why, and the fix

The catch block printed a line, but left settings undefined, so the program fails a few lines later with a less helpful error. A catch must leave the program in a usable state: give settings a default value there, or rethrow the error if the program cannot go on.

JavaScript in the browser: your browser’s own engine, in a sandboxed worker. Syntax errors are located with acorn 8.18.0, MIT. Licence and source

Exit ticket

5 questions, no hints. Score 80% or more to complete the lesson.

Finish every activity above to unlock the exit ticket.

Report a problem

Spotted something wrong or unclear? Say what, and it will be checked and fixed.

#

At least 20 characters.

Only if you want a reply.

Key ideas

throw stops the function

throw new Error("Out of stock") stops the current function at once: no statement after it runs, and no value is returned. The error travels up to the nearest try...catch around the call, in this function or any caller. If there is none, the program stops with the error. Throw Error objects, not strings: an Error has a name, a message and a stack, and built-in types such as TypeError and RangeError say what kind of problem it is.

Catch what you can handle

try { … } catch (error) { … } runs the catch block when anything in the try block throws, including functions it calls. error.name and error.message describe it, and error instanceof SyntaxError checks its type. Handle only the errors you expect, such as broken JSON from a user, and throw everything else on with throw error; a catch that swallows every error hides bugs. If you do not need the error, write catch { … } without the parentheses.

finally always runs

finally { … } runs after the try block, and after the catch block if one ran, whether an error was thrown or not, and even when try or catch returns. It is the place for cleanup: close a file, hide a spinner, unlock a button. The value of a return in try is decided first; finally runs before the function gives it back. Do not return or throw from finally itself: that replaces the earlier return or error.

Sources

Last reviewed October 5, 2026