cf.completefrontendCode editorOpen lab
THE JAVASCRIPT FIELD GUIDE

Error handling strategies

Decide when JavaScript should fail fast, recover, report uncaught errors, isolate broken UI, log safely, and return result objects instead of surprise exceptions.

By the end, you can
  • 01
    Choose a strategyDecide when to fail fast and when to recover with reduced behavior.
  • 02
    Contain failuresUse global handlers and render boundaries without breaking the page.
  • 03
    Report safelyLog useful context, redact secrets, and model expected failures with result objects.

Strategy before syntax

The previous lessons, try, catch & finally and Throwing & custom errors, taught the mechanics: catch a failure, throw a better one, attach a cause, and clean up. This lesson answers the next question: what should happen after something goes wrong?

Professional JavaScript is not error-free JavaScript. It is code that fails in the right place, keeps users safe, reports enough information to fix the bug, and avoids turning a small problem into a bigger one. Sometimes that means stopping immediately. Sometimes it means showing a fallback. Sometimes it means returning an object that says failure without throwing at all.

Five error handling strategies
StrategyUse it whenWhat to do
Fail fastInvalid state would corrupt data or hide a programming mistake.Throw, stop the current operation, show a clear failure, and fix the cause.
RecoverThe user can continue safely with a fallback, default, cached value, or disabled feature.Catch locally, use a reduced mode, and tell logs what happened.
Global handlerNo local catch handled the problem.Record the final alarm and show a generic message. Do not use it for normal control flow.
BoundaryOne UI region failed while the rest of the page can remain useful.Catch around that render/task and show a fallback for only that region.
Result objectFailure is expected and callers should choose what to do next.Return { ok: true, value } or { ok: false, error } instead of throwing.
The lesson definition

An error handling strategy is a deliberate choice about where a failure is caught, what the user sees, what developers learn, and whether the program stops or continues in a reduced mode.

We will keep promises small here: unhandledrejection gets one practical paragraph and a tiny Promise.reject demo. The full promise model comes later in Stage 6, in the Promises and Error handling with promises lessons.

Fail fast vs recover

INTERACTIVE

Fail fast means stop as soon as code detects an unsafe state. It is not pessimism; it is containment. A bad checkout total, a missing security token, or an impossible configuration should not be guessed around. Throw, stop the current operation, and make the bug visible while it is still small.

Real-life analogyFail fast is a smoke detector

A smoke detector is annoying on purpose. It does not quietly open a window and hope. It screams so humans can act before the kitchen is gone. Failing fast does the same for impossible program states.

In real life: Smoke appears
In JavaScript: Invalid state appears
In real life: The alarm screams immediately
In JavaScript: Code throws immediately
In real life: People investigate while the fire is small
In JavaScript: Developers fix the bug before data is corrupted

Where the analogy stops: A smoke detector only reports. Code may also clean up resources or wrap an error with more context before it rethrows.

Recovering means the program can continue honestly in a reduced mode. A missing avatar can become initials. A saved theme preference can fall back to the default theme. A recommendations panel can disappear while the article remains readable.

Real-life analogyRecovering is a spare tire

Recovery is not pretending nothing happened. It is a safe, reduced path that keeps the user moving. The trick is knowing whether the spare tire is safe enough for this road.

In real life: A flat tire
In JavaScript: A feature or input fails
In real life: A spare tire
In JavaScript: A fallback value, cache, or simpler UI
In real life: Drive slowly to a repair shop
In JavaScript: Keep users moving while logging the problem

Where the analogy stops: A spare tire is temporary. Recovery should not hide the original problem forever; it should be logged and repaired.

The playground below parses a small configuration. Strict mode throws when required data is missing. Lenient mode uses defaults and returns a warning. Both are valid strategies in different products.

Fail fast or recover from bad config
Step 0 of 5Ready
Your turn: follow the blue line

Step through the same bad config in strict or lenient mode. Strict fails fast; lenient recovers with defaults and a warning.

Running in
  1. script
Next: line 1
Click the blue line to take the next stepPop out in the code editor (opens in a new tab)JavaScript
 function parseConfig(text, mode) {  try {    const draft = JSON.parse(text);    if (typeof draft.endpoint !== "string") throw new Error("endpoint is required");    if (!Number.isInteger(draft.retries) || draft.retries < 0) throw new Error("retries must be a non-negative integer");    return { ok: true, config: { ...defaults, ...draft }, warnings: [] };  } catch (error) {    if (mode === "strict") throw error;    return { ok: true, config: defaults, warnings: [error.message] };  }} parseConfig("{ \"retries\": 2, \"cache\": true }", "strict");
CallStoreChangeResultRun = next line. Ran = already executed.
Recent returnsNothing yet. Start with the blue line.
Change the strategy used on line 10 or 11

Changing the strategy starts a fresh replay of the missing-endpoint case.

A guided replay recorded from real JavaScript calls, not an engine debugger. Step follows executed statements; Back reviews a snapshot. Reset starts a fresh run.
Try real config inputs
Parser sourcePop out in the code editor (opens in a new tab)JavaScript
const defaults = { endpoint: "/api", retries: 3, cache: false }; function parseConfig(text, mode) {  try {    const draft = JSON.parse(text);    if (typeof draft.endpoint !== "string") throw new Error("endpoint is required");    if (!Number.isInteger(draft.retries) || draft.retries < 0) throw new Error("retries must be a non-negative integer");    return { ok: true, config: { ...defaults, ...draft }, warnings: [] };  } catch (error) {    if (mode === "strict") throw error;    return { ok: true, config: defaults, warnings: [error.message] };  }}
Real resultstrict
{ "retries": 2, "cache": true }

Stopped result

{
  "ok": false,
  "error": "endpoint is required"
}
Try it yourself
Input:

Stopped: endpoint is required

This free-play panel runs the same parser with several real strings. Strict mode stops on bad input; lenient mode falls back to defaults with warnings.
Fail fast or recover?
  • A checkout total is NaN right before charging a card
  • A profile photo CDN is slow, but initials are available
  • A required security token is missing from a form submission
  • Reading a saved theme setting fails
  • A database migration precheck sees an impossible schema version
  • A recommendation widget cannot load on an article page
Try it yourself
0 of 6 correct

Sort each scenario by the safer strategy.

Choose a category for every card. You can change an answer at any time; Reset clears them all.

Global handlers: error & unhandledrejection

SANDBOXED

Local code should catch errors where it has enough context to recover or explain the problem. A browser global handler is the last safety net after that local handling did not happen. In most browsers, window.addEventListener("error", handler) receives an uncaught error event with fields such as message, filename, and lineno.

A second browser event, unhandledrejection, reports a Promise that rejects and is not handled. Minimal example: Promise.reject(new Error("No handler"));. Do not worry if promises still feel mysterious; they are taught in Stage 6. For now, remember that this event is the promise-shaped version of the central alarm panel.

Real-life analogyGlobal handlers are the central alarm panel

A building has local alarms and one central panel. If nobody handles a room alarm, the panel records it so people can respond. Browser global handlers fill that final reporting role.

In real life: A room alarm nobody answered
In JavaScript: An error nobody caught locally
In real life: The panel logs room and alarm type
In JavaScript: The handler logs message, file, line, or rejection reason
In real life: Building staff still need fire doors and procedures
In JavaScript: Apps still need local catch blocks and boundaries

Where the analogy stops: The alarm panel is too late to save work inside the room. Global handlers are best for reporting, not normal recovery.

Sandboxed global-handler lab
Iframe listenersPop out in the code editor (opens in a new tab)JavaScript
window.addEventListener("error", (event) => {  parent.postMessage({    kind: "error",    message: event.message,    filename: event.filename,    lineno: event.lineno,  }, "*");}); window.addEventListener("unhandledrejection", (event) => {  parent.postMessage({    kind: "unhandledrejection",    reason: String(event.reason && event.reason.message || event.reason),  }, "*");});
Sandbox frameallow-scripts
Events received

No alarms yet.

Try it yourself

The listeners are installed only inside the sandboxed iframe. Trigger an error or rejection to see the event details posted back.

The parent page validates event.source before accepting a message. The iframe is sandboxed with scripts only, so the demo cannot break the lesson page.

Error boundaries

STEP THROUGH

An error boundary is a protective wrapper around a region of UI. In a framework, you can think of it as a component-level try/catch for rendering: if one child fails, the boundary shows a fallback for that area instead of letting the whole screen disappear.

Real-life analogyAn error boundary is a fire door

A fire door does not make fire good. It keeps one burning room from taking down the whole house. Boundaries apply that idea to UI.

In real life: One room catches fire
In JavaScript: One widget throws while rendering
In real life: The fire door closes
In JavaScript: The boundary catches that widget’s error
In real life: The rest of the house stays usable
In JavaScript: Neighboring widgets still render

Where the analogy stops: A fire door does not repair the room. A boundary still needs logging and a fallback that tells the truth.

React has a formal error-boundary feature, but this course has not taught React yet. So the demo uses plain JavaScript: three widget render functions, one of which throws. The boundary wraps each widget separately.

Step through a widget boundary
Step 0 of 5Ready
Your turn: follow the blue line

Step through a plain-JS boundary around three widget render functions. One throws, but the others survive.

Running in
  1. script
Next: line 11
Click the blue line to take the next stepPop out in the code editor (opens in a new tab)JavaScript
function renderWidget(widget) {  return "<article>" + widget.title + ": " + widget.render() + "</article>";} function renderWithBoundary(widget) {  try {    return renderWidget(widget);  } catch (error) {    return "<article>" + widget.title + ": temporarily unavailable</article>";  } widgets.map(renderWithBoundary);
CallStoreChangeResultRun = next line. Ran = already executed.
Recent returnsNothing yet. Start with the blue line.
A guided replay recorded from real JavaScript calls, not an engine debugger. Step follows executed statements; Back reviews a snapshot. Reset starts a fresh run.
Widget dashboard boundary demo
Boundary patternPop out in the code editor (opens in a new tab)JavaScript
function renderWidget(widget) {  return "<article>" + widget.title + ": " + widget.render() + "</article>";} function renderWithBoundary(widget) {  try {    return renderWidget(widget);  } catch (error) {    return "<article>" + widget.title + ": temporarily unavailable</article>";  }} widgets.map(renderWithBoundary);
Dashboardboundary on
Sales

₹12,400

Alerts

Temporarily unavailable

Alerts service offline
Uptime

99.98%

Try it yourself

The Alerts widget throws, but the boundary catches it and returns one fallback card. Sales and Uptime still render.

This is a framework-neutral model of an error boundary: a component-level try/catch for rendering.

Logging & monitoring

PRACTICAL

A handled error that nobody can diagnose is only half handled. Good logging answers: what failed, where, during which user action, in which release, and with what safe context? Useful fields include message, stack, route or screen, user action, feature flag, browser information when available, and release version.

Logs must also be safe. Do not send passwords, tokens, secrets, credit card data, or full personal data just because it was nearby. Redact first, sample repeated noise when volume is high, and use monitoring services such as Sentry or similar tools neutrally: they collect, group, alert, and connect events to releases; your code still decides what is appropriate to send.

Safe logger shapePop out in the code editor (opens in a new tab)JavaScript
function redactSecrets(context) {
  const clone = { ...context };
  for (const key of Object.keys(clone)) {
    if (/token|password|secret/i.test(key)) clone[key] = "[redacted]";
  }
  return clone;
}

function logError(error, context) {
  return {
    message: error.message,
    stack: error.stack,
    context: redactSecrets(context),
    release: "2026.09.25",
  };
}
Monitoring is not magic

A monitoring service can group repeated stack traces and alert a team, but it cannot know your privacy policy or whether a fallback is truthful. Keep strategy decisions in your code.

Result objects

STEP THROUGH

Throwing is excellent for surprising failures and invalid states. But some failures are expected: user text may not be valid JSON, a search may find no match, a retryable operation may fail twice before succeeding. In those cases, a result object can make the success and failure paths explicit.

Real-life analogyA result object is a sealed envelope

Instead of a surprise exception flying across the room, the function hands back a sealed envelope. The outside says whether it contains a value or an error.

In real life: The envelope label says success
In JavaScript: { ok: true, value }
In real life: The envelope label says failure
In JavaScript: { ok: false, error }
In real life: You read the label before reaching inside
In JavaScript: Callers check result.ok before using result.value

Where the analogy stops: An envelope is passive. Code must still decide whether to retry, show a message, log, or rethrow.

Open the result-object envelope
Step 0 of 4Ready
Your turn: follow the blue line

Follow a result object from a parse attempt to a safe success branch.

Running in
  1. script
Next: line 9
Click the blue line to take the next stepPop out in the code editor (opens in a new tab)JavaScript
function tryParse(text) {  try {    return { ok: true, value: JSON.parse(text) };  } catch (error) {    return { ok: false, error };  }} if (result.ok) {  console.log(result.value.name);}
CallStoreChangeResultRun = next line. Ran = already executed.
Recent returnsNothing yet. Start with the blue line.
A guided replay recorded from real JavaScript calls, not an engine debugger. Step follows executed statements; Back reviews a snapshot. Reset starts a fresh run.
Result object free play
tryParse patternPop out in the code editor (opens in a new tab)JavaScript
function tryParse(text) {  try {    return { ok: true, value: JSON.parse(text) };  } catch (error) {    return { ok: false, error };  }} const result = tryParse('{ "name": "Ada" }');if (result.ok) {  console.log(result.value.name);}
Envelopesuccess
{
  "ok": true,
  "value": {
    "name": "Ada"
  }
}
Try it yourself

The envelope says ok: true, so callers may read value.

This panel mirrors tryParse: expected parse failures become explicit data.

Result objects combine well with small helpers. A retry helper can call an operation several times and stop at the first ok: true result without using exceptions as loop control.

Retry with result objectsPop out in the code editor (opens in a new tab)JavaScript
function retry(operation, attempts = 3) {
  let last;
  for (let attempt = 1; attempt <= attempts; attempt += 1) {
    const result = operation();
    if (result.ok) return result;
    last = result;
  }
  return last;
}

Where you’ll use this

Error strategy shows up anywhere JavaScript talks to users, data, or other systems. The pattern is to catch as close as possible to the place that can make a good decision, then let truly unexpected failures rise to a boundary or global handler for reporting.

Common places to choose a strategy
PlaceLikely strategyExample
Form validationRecover locallyShow field messages; do not submit invalid data.
Payment calculationFail fastStop if total is NaN or currency is missing.
Dashboard widgetBoundaryShow one fallback card and keep neighboring widgets visible.
User-provided JSONResult objectReturn { ok: false, error } and let the caller choose a message.
Uncaught browser failureGlobal handlerRecord message, stack, action, and release safely.

If you want to revisit reading stack traces, the published Reading errors and Debugging lessons are good companions. Closures can also help when you create a logger that remembers release information; see the Closures lesson.

Common misconceptions

  • “Catching means fixed.” A catch block must recover honestly, add context and rethrow, or report safely.
  • “Global handlers can replace local handling.” They are final alarms, usually too late for specific recovery.
  • “Recover whenever possible.” Unsafe recovery is worse than a loud failure.
  • “Error boundaries are only a React trick.” React has an API, but the idea is broader: isolate one UI region.
  • “Log everything.” Log enough to debug, but redact secrets and sample noisy repeats.
  • “Result objects are always better than throws.” Use them for expected failures; throw for impossible states and bugs.

Practice exercises

5 EXERCISES
Exercise 1 · Warm-upConvert throwing code to a result object

Run the starter code mentally. What does it print for the valid JSON input?

Starter codePop out in the code editor (opens in a new tab)JavaScript
function parseCount(text) {
  try {
    return { ok: true, value: JSON.parse(text).count };
  } catch (error) {
    return { ok: false, error };
  }
}
const result = parseCount('{ "count": 3 }');
console.log(result.ok ? result.value : "bad");

Answer, then press Check. Spacing and letter case don’t matter.

    Exercise 2 · Warm-upChoose the strategy

    A checkout is about to charge a card, but the total is NaN. Should it fail fast or recover?

    Starter codePop out in the code editor (opens in a new tab)JavaScript
    const total = Number.NaN;
    console.log(Number.isNaN(total) ? "fail fast" : "recover");

    Answer, then press Check. Spacing and letter case don’t matter.

      Exercise 3 · PracticeWrap widgets in a boundary

      Complete the dashboard pattern so one throwing widget cannot stop its neighbors.

      Starter codePop out in the code editor (opens in a new tab)JavaScript
      const widgets = [salesWidget, alertsWidget, uptimeWidget];
      const cards = widgets.map((widget) => {
        // Wrap one widget render here.
      });
        Exercise 4 · PracticeWrite a safe logger that redacts tokens

        What should the safe logger print for authToken?

        Starter codePop out in the code editor (opens in a new tab)JavaScript
        function redactSecrets(context) {
          const clone = { ...context };
          for (const key of Object.keys(clone)) {
            if (/token|password|secret/i.test(key)) clone[key] = "[redacted]";
          }
          return clone;
        }
        console.log(redactSecrets({ userId: 7, authToken: "abc", password: "p" }).authToken);

        Answer, then press Check. Spacing and letter case don’t matter.

          Exercise 5 · ChallengeExplain what unhandledrejection catches

          Name the event that catches a rejected promise when no code handles that rejection.

          Answer, then press Check. Spacing and letter case don’t matter.

            Check your understanding

            7 QUESTIONS
            Error handling strategies quiz · 7 questionsScore: first tries count
            1. Question 1 of 7When should code fail fast?

              Choose an answer to see the explanation.

            2. Question 2 of 7What does the tryParse result code print?

              Read the code, then predictPop out in the code editor (opens in a new tab)JavaScript
              function tryParse(text) {
                try { return { ok: true, value: JSON.parse(text) }; }
                catch (error) { return { ok: false, error }; }
              }
              const result = tryParse("not json");
              console.log(result.ok ? "value" : "failure");

              Choose an answer to see the explanation.

            3. Question 3 of 7What should a browser error or unhandledrejection listener be used for?

              Choose an answer to see the explanation.

            4. Question 4 of 7In this lesson’s plain-JS boundary, what happens when one widget throws?

              Choose an answer to see the explanation.

            5. Question 5 of 7What does the redaction code print?

              Read the code, then predictPop out in the code editor (opens in a new tab)JavaScript
              function redact(context) {
                const copy = { ...context };
                if ("apiToken" in copy) copy.apiToken = "[redacted]";
                return copy;
              }
              console.log(redact({ apiToken: "secret", action: "save" }).apiToken);

              Choose an answer to see the explanation.

            6. Question 6 of 7Why use a result object?

              Choose an answer to see the explanation.

            7. Question 7 of 7What does the retry loop code print?

              Read the code, then predictPop out in the code editor (opens in a new tab)JavaScript
              let tries = 0;
              function operation() {
                tries += 1;
                return tries < 2 ? { ok: false, error: new Error("wait") } : { ok: true, value: "done" };
              }
              let result;
              for (let i = 0; i < 3; i += 1) {
                result = operation();
                if (result.ok) break;
              }
              console.log(result.value);

              Choose an answer to see the explanation.

            Key takeaways

            • Fail fast when continuing would corrupt data, hide a bug, or be unsafe.
            • Recover when there is a truthful fallback or reduced mode.
            • Global error and unhandledrejection handlers are final reporting alarms, not a replacement for local strategy.
            • Error boundaries isolate one UI region so the rest can keep working.
            • Logs should include message, stack, context, user action, and release, with secrets redacted.
            • Result objects make expected success and failure explicit: { ok: true, value } or { ok: false, error }.

            Error handling strategy means catching failures at the narrowest place that can make a safe decision, and reporting everything else clearly.

            Up next: Synchronous vs asynchronous.

            CompleteFrontend Clear concepts. Working examples.