Skip to content
Foxipa

Suggest an edit

Tell us what should be corrected, clarified, or improved. Submission will be connected to Foxipa’s contribution service later.

No information is sent anywhere yet. Your suggestion is only prepared in your browser.

Learning

JavaScript Errors and Defensive Control Flow

Handle JavaScript exceptions deliberately: throw and catch Error objects, preserve causes, clean up with finally, define custom errors, aggregate failures, and design defensive control-flow boundaries.

Levels: Intermediate ~35 min read

Intermediate

JavaScript exceptions are a control-flow mechanism: an operation can produce an abrupt completion, and the language can transfer control to a matching catch clause. Good error handling is not just wrapping code in try/catch. You need to distinguish programmer errors from expected failures, preserve useful cause information, clean up reliably, and choose where an error should be handled.

Throwing is an explicit control-flow transfer

The throw statement evaluates its expression and immediately produces a throw completion. JavaScript can throw any value, but application code should normally throw Error objects or subclasses so callers receive a predictable error shape and stack information.

Interactive Runnable example
Edit

Ready to run.

Quick check

What does throw do?

throw new Error("failed");

Error objects carry structured information

The standard Error object exposes name and message through its prototype, while engines commonly provide stack information as implementation-defined debugging data. Error options can also provide a cause, allowing a higher-level error to preserve the lower-level failure without replacing it with an unrelated message.

Interactive Runnable example
Edit

Ready to run.

Quick check

Why use the cause option?

new Error("outer", { cause: originalError });

try/catch defines an exception boundary

A try block encloses code where a throw completion may occur. If the try block completes normally, its catch clause is skipped. If it throws, the thrown value becomes the catch binding and the catch block gets a chance to recover, transform, log, or rethrow it.

Interactive Runnable example
Edit

Ready to run.

Quick check

When does catch run?

try { throw new Error("x"); } catch (error) { /* handles x */ }

Catch bindings are scoped

A catch parameter is a lexical binding local to the catch clause. Optional catch binding lets you handle an error without naming it when the value itself is not needed.

Interactive Runnable example
Edit

Ready to run.

Quick check

When is catch without a binding useful?

try { risky(); } catch { recover(); }

finally is for code that must run

A finally block runs after the try/catch path whether control leaves the protected code normally or abruptly. It is especially useful for cleanup. A return, throw, break, or continue inside finally can replace an earlier completion, so finally should normally perform cleanup rather than decide unrelated application results.

Interactive Runnable example
Edit

Ready to run.

Quick check

What is a typical purpose of finally?

try { acquire(); } finally { release(); }

Do not accidentally override a useful completion in finally

Because finally participates in completion propagation, a return or throw inside finally can replace a value or error that was already in flight. This is a subtle source of swallowed failures and should generally be avoided in cleanup code.

Interactive Runnable example
Edit

Ready to run.

Quick check

Why should return inside finally be avoided?

function f() { try { throw new Error("x"); } finally { return "hidden"; } }

Catch the failures you can actually handle

A broad catch block can turn useful failures into silent corruption. Prefer handling errors at the boundary where recovery is meaningful. When a failure cannot be handled safely, rethrow it so a higher-level boundary can decide what to do.

Interactive Runnable example
Edit

Ready to run.

Quick check

What should happen to an error the current layer cannot safely handle?

catch (error) { if (error instanceof TypeError) recover(); else throw error; }

Use error classes to express categories

Custom Error subclasses let callers distinguish categories with instanceof while retaining the standard Error behavior. Set the name when a custom category benefits from a stable descriptive name, and pass a message plus optional cause when wrapping another failure.

Interactive Runnable example
Edit

Ready to run.

Quick check

What is the main benefit of a custom Error subclass?

class ValidationError extends Error {}

AggregateError represents multiple failures

AggregateError groups multiple errors into one Error object. Its errors property contains the collected errors, which is useful when an operation intentionally evaluates multiple independent tasks and wants to report their failures together.

Interactive Runnable example
Edit

Ready to run.

Quick check

What does AggregateError provide?

throw new AggregateError([errorA, errorB], "batch failed");

Error.isError checks Error objects more directly

Current ECMAScript includes Error.isError. It checks whether a value is an Error object according to the language specification, rather than requiring a particular constructor identity from a single realm. It is useful when the question is “is this an Error object?” rather than “is this instance of my particular subclass?”

Interactive Runnable example
Edit

Ready to run.

Quick check

What question does Error.isError answer?

Error.isError(new Error("x"));

Rethrowing preserves the right boundary

A catch block can add context and rethrow the same error, or throw a new error with cause. This lets low-level code add useful context without pretending it has enough information to recover.

Interactive Runnable example
Edit

Ready to run.

Quick check

What is a good way to add higher-level context?

catch (error) { throw new Error("load failed", { cause: error }); }

Cleanup and resource lifetime

When code owns a resource, cleanup belongs near the ownership boundary. try/finally is the classic synchronous mechanism. If an operation is asynchronous, cleanup must also respect the asynchronous lifetime of the resource; try/finally can still be used around await-based code.

Interactive Runnable example
Edit

Ready to run.

Quick check

Where should resource cleanup normally live?

acquire(); try { use(); } finally { release(); }

Defensive control flow is not “catch everything”

Defensive code validates boundaries, chooses meaningful error categories, preserves causes, cleans up deterministically, and lets unrecoverable failures propagate. Catching every error and returning a default value can hide defects and make later failures harder to diagnose.

Interactive Runnable example
Edit

Ready to run.

Quick check

Which strategy is most defensive?

function save(value) { if (value == null) throw new TypeError("value required"); }

Specification mental model: completions

ECMAScript specifies many operations in terms of completion records. Normal completion carries a value; abrupt completion represents control flow such as throw, return, break, or continue. try/catch/finally is defined in terms of these completions, which explains why finally can replace an earlier result.

Interactive Runnable example
Edit

Ready to run.

Quick check

Why does the completion model matter to authors?

try { return 1; } finally { /* cleanup */ }

Authoring rules

Prefer Error objects over arbitrary thrown primitives. Give custom failures stable categories. Catch only where you can recover or add useful context. Preserve lower-level failures with cause. Put cleanup in finally. Avoid return or throw from finally unless deliberately replacing an earlier completion. Do not turn unexpected failures into silent defaults.

Practice: design an error boundary

Build a small function that parses configuration, throws ValidationError for bad input, wraps an unexpected lower-level failure with cause, and uses a caller-level catch to display a safe message. Then add a finally block that records cleanup. Trace which layer owns recovery and which layer merely adds context.

Specification references

Primary language authority: ECMA-262, current ECMAScript specification. Relevant sections include ThrowStatement and the try statement, Error objects, native Error types, AggregateError, Error.isError, and InstallErrorCause. Current specification URL: https://tc39.es/ecma262/multipage/ .

Was this page helpful?

Mark this learning resource complete to track your learning progress.