Errors: custom types, cause, finally, and failing async work
Throw Error objects, subclass them so callers can tell failures apart, chain them with cause, and catch only what you can handle — sync or async.
After this lesson you can
- Define an Error subclass and branch on it with instanceof
- Wrap a low-level error with cause instead of losing it
- Predict what finally does to a return value
- Retry an async operation without swallowing errors that should not be retried
throw accepts any value, but only an Error (or a subclass) carries a
stack — the record of where it was created — and a name and
message that logging tools understand. throw "not found" gives the
catcher a bare string with no trace of where it came from. Always throw
Error objects.
Your own error types
A subclass lets callers tell failures apart without parsing messages:
class ValidationError extends Error {
constructor(field, message, options) {
super(message, options);
this.name = "ValidationError";
this.field = field;
}
}
Set name — otherwise it is inherited as "Error" and every log line
reads the same. Callers branch with instanceof, not on the message
text.
cause: wrap without losing the original
When you catch a low-level error and throw a more meaningful one, pass
the original as cause: new Error("config unreadable", { cause: err }).
The new error describes what failed at your level; err.cause still
holds the exact underlying failure for whoever debugs it.
class ValidationError extends Error { constructor(field, message, options) { super(message, options); this.name = "ValidationError"; this.field = field; }} function parsePort(text) { let value; try { value = JSON.parse(text).port; } catch (err) { throw new ValidationError("port", "config is not valid JSON", { cause: err }); } if (!Number.isInteger(value)) { throw new ValidationError("port", `port must be an integer, got ${JSON.stringify(value)}`); } return value;} function describe(text) { try { return { port: parsePort(text) }; } catch (err) { return { name: err.name, message: err.message, isValidation: err instanceof ValidationError, isError: err instanceof Error, causeName: err.cause ? err.cause.name : null, hasStack: typeof err.stack === "string", }; }} export function run() { let thrownString; try { throw "not found"; } catch (err) { thrownString = { type: typeof err, hasStack: typeof err?.stack === "string" }; } return { good: describe('{"port": 8080}'), wrongType: describe('{"port": "8080"}'), notJson: describe("port=8080"), thrownString, };}Catch what you can handle
JavaScript has no typed catch clauses. A catch receives everything —
your validation errors, but also a TypeError from a bug two calls
down. Handle the cases you expect and rethrow the rest:
try {
port = parsePort(text);
} catch (err) {
if (!(err instanceof ValidationError)) throw err; // not ours: let it go
port = 8080;
}
A catch that logs and continues for every error turns real bugs into
silently wrong results.
finally
finally runs whether the try returned, threw or completed normally —
the place for cleanup such as closing a file or releasing a lock. It
does not change the value being returned, unless it contains its own
return, which replaces the result and even discards an error that was
on its way out.
export async function run() { const log = []; function readConfig() { try { log.push("try"); return "value from try"; } finally { log.push("finally (cleanup)"); } } function swallow() { try { throw new Error("this error is lost"); } finally { return "finally returned"; } } async function failLater() { throw new Error("async failure"); } const value = readConfig(); const swallowed = swallow(); let asyncCaught; try { await failLater(); } catch (err) { asyncCaught = err.message; } return { log, value, swallowed, asyncCaught };}Async errors
Inside an async function a throw becomes a rejected promise, and an
awaited rejection becomes a throw at the await (lesson 5). So the
same try/catch works — as long as the promise is awaited inside
the try. return task() without await hands the promise back
before it settles, and a rejection then escapes the catch around it.
Common mistakes
- Catching everything and retrying or ignoring it. A bug such as a
TypeErrordeserves to surface, not to be retried three times. - Rethrowing a new error without
cause. The original stack is gone, and whoever debugs it starts from your message alone. returninsidefinally. It overrides thetry's result and swallows a thrown error, as the second example shows.return promiseinstead of inside a . The rejection happens after the has finished.
Exercise · Pro
Retry only what is worth retrying
Exercises are free in the first 3 lessons of every course. From here on they are part of Pro and Sprint; the lesson and its runnable examples stay open.
Upgrade