JavaScript Basics: Handle Errors Without Hiding Them
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 11. 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.mjsOutput:
{"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.mjsOutput:
Message: Cannot read settings
Cause: SyntaxErrorThe 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.mjsOutput:
Failure preserved: Task failed
Cleanup ran: trueAwaiting 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_dirAcknowledgements
This post was created with assistance from AI.