Skip to content
JavaScript Basics: Handle Errors Without Hiding Them

JavaScript Basics: Handle Errors Without Hiding Them

October 17, 2026

An error handler should recover from failures it understands and preserve failures it cannot handle. These small examples cover selective catching, error causes, and cleanup.

These examples use built-in features and were tested on Linux with Node.js v24.19.0. No packages are needed. Assertions check the results before the program finishes.

Prepare a scratch directory

You need Bash, a text editor, Node.js on your PATH, and the mktemp, rm, and rmdir utilities. Keep using the same terminal. These commands stop the shell if setup fails. Save each named source file as UTF-8; .mjs selects ES modules.

start_dir="$PWD"
work_dir="$(mktemp -d)" || exit 1
cd -- "$work_dir" || exit 1

1. Recover from a known error

Save this as selective-catch.mjs:

import assert from 'node:assert/strict';

function parseOrDefault(source) {
  try {
    return JSON.parse(source);
  } catch (error) {
    if (!(error instanceof SyntaxError)) throw error;
    return { enabled: false };
  }
}
assert.deepEqual(parseOrDefault('{"enabled":true}'), { enabled: true });
assert.deepEqual(parseOrDefault('{broken'), { enabled: false });
console.log(JSON.stringify(parseOrDefault('{"enabled":true}')));
console.log(JSON.stringify(parseOrDefault('{broken')));

Run it:

node selective-catch.mjs

Output:

{"enabled":true}
{"enabled":false}

This example deliberately falls back only for JSON syntax errors. That policy may suit optional settings, but would be inappropriate for data that must be valid. Other exceptions are rethrown. The try/catch statement handles synchronous exceptions in its protected block.

2. Add context while retaining the cause

Save this as error-context.mjs:

import assert from 'node:assert/strict';

function loadSettings(source) {
  try {
    return JSON.parse(source);
  } catch (cause) {
    throw new Error('Cannot read settings', { cause });
  }
}
try {
  loadSettings('{broken');
  assert.fail('Expected parsing to fail');
} catch (error) {
  assert.equal(error.message, 'Cannot read settings');
  assert.equal(error.cause instanceof SyntaxError, true);
  console.log(`Message: ${error.message}`);
  console.log(`Cause: ${error.cause.name}`);
}

Run it:

node error-context.mjs

Output:

Message: Cannot read settings
Cause: SyntaxError

The cause option preserves the original failure while the new message describes the operation. Avoid asserting engine-specific parser messages or logging sensitive configuration contents.

3. Run cleanup after an awaited failure

Save this as awaited-cleanup.mjs:

import assert from 'node:assert/strict';

let cleanedUp = false;
async function runTask() {
  try {
    await Promise.reject(new Error('Task failed'));
  } finally {
    cleanedUp = true;
  }
}
await assert.rejects(runTask(), { message: 'Task failed' });
assert.equal(cleanedUp, true);
console.log('Failure preserved: Task failed');
console.log(`Cleanup ran: ${cleanedUp}`);

Run it:

node awaited-cleanup.mjs

Output:

Failure preserved: Task failed
Cleanup ran: true

Awaiting the promise makes its rejection enter this control flow. The finally block runs before the async function settles. Here cleanup succeeds, so the original error survives. A throw or return in finally can replace an earlier result or failure. A surrounding try does not catch a later timer callback or an unawaited promise rejection.

Clean up

In the same terminal, remove only the example files, return to your original directory, and remove the empty scratch directory:

rm -- selective-catch.mjs error-context.mjs awaited-cleanup.mjs
cd -- "$start_dir" || exit 1
rmdir -- "$work_dir"
unset start_dir work_dir

Acknowledgements

This post was created with assistance from AI.

Last updated on