Skip to the chapter
←JavaScriptGGroundworkJSJavaScript
JavaScript/Intermediate/I7
24 / 41 written chapters
/

The cover
  1. B1Setup & mental model
  2. B2Execution context
  3. B3var, let & const
  4. B4Scope
  5. B5Single thread
  6. B6In the browser
  7. B7Types & values
  8. B8Strings & Math
  9. B9Operators & flow
  10. B10Functions
  11. B11Objects & arrays
  12. B12this
  13. B13Closures
  14. B14DOM & events
  15. B15Callbacks & promises
  16. B16Errors & tools
  17. B17Guided project
  18. ★Cheat page
  1. I1Scope & functions, properly
  2. I2Objects deeply
  3. I3Prototypes & OOP
  4. I4Async, properly
  5. I5Modules & tooling
  6. I6Regex & dates
  7. I7Error handling
  8. I8Real-time connections
  9. I9Offline & storage
  10. I10Browser APIs
  11. I11Modern JS
  12. ★Cheat page
  1. A1Engine & memory
  2. A2Advanced async
  3. A3Metaprogramming
  4. A4Types & data
  5. A5Patterns & architecture
  6. A6Performance
  7. A7Security
  8. A8Ecosystem
  9. A9Testing
  10. A10Observability
  11. A11Interview bank
  12. A12Build it yourself
  13. ★Cheat page

JSI7 · Part 2 · Intermediate

Error handling & debugging

Past what the beginner errors chapter covered — error chains, lost rejections and real debugging tools.

6 min read6 sections2 exercises
24 of 41

In this chapter

  • Error chaining with cause
  • Unhandled promise rejections
  • window.onerror — the other global handler
  • AggregateError — when there's more than one error to report
  • Immutability — why frameworks care so much

and 1 more

about 6 min
06On this page
  1. Error chaining with cause
  2. Unhandled promise rejections
  3. window.onerror — the other global handler
  4. AggregateError — when there's more than one error to report
  5. Immutability — why frameworks care so much
  6. Debugging, past console.log

The first pass at errors covered try/catch/finally, custom Error subclasses, and reading a stack trace. This is what's past that: chaining errors together, what happens to a rejection nobody catches, why immutability keeps coming up in framework code, and debugging tools past console.log.

Error chaining with cause

Catching a low-level error and throwing a more meaningful one is normal — but doing that used to destroy the original error entirely. The cause option keeps it attached.

function loadUser() {
  try {
    JSON.parse("not valid json");
  } catch (dbError) {
    throw new Error("failed to load user", { cause: dbError });
  }
}

try {
  loadUser();
} catch (e) {
  console.log(e.message);
  console.log(e.cause.message);   // what happens?
}

"failed to load user", then the original SyntaxError's message. Without cause, that original error is just gone — whoever's debugging this in production sees "failed to load user" and has to guess why. With it, e.cause carries the full original error (and its own stack trace) all the way up, however many layers re-throw in between.

Dry run: tracing loadUser()'s catch and cause chain

StepCodeWhat happens
1loadUser() is calledenters the outer try block
2JSON.parse("not valid json")throws a SyntaxError — the string isn't valid JSON
3inner catch (dbError)dbError is bound to that SyntaxError
4throw new Error("failed to load user", { cause: dbError })a brand-new Error is thrown, with dbError attached as its .cause — it propagates out of loadUser()
5outer catch (e)e is the new "failed to load user" error; e.cause is still the original SyntaxError
6console.log(e.message)"failed to load user"
7console.log(e.cause.message)the original SyntaxError's message — never lost, never printed until now

Two separate try/catch pairs, two separate errors — cause is what lets the second error carry a live reference to the first instead of replacing it outright.

Unhandled promise rejections

A rejected promise with no .catch() anywhere in its chain doesn't fail silently — it surfaces as a top-level unhandledrejection event (the same mechanism this site's own code runner listens to, to show you an error even from code with no explicit catch at all).

window.addEventListener("unhandledrejection", (event) => {
  console.error("Unhandled:", event.reason);
  event.preventDefault();   // stops it from also logging as a browser console error
});
⚠ Forgetting to return inside a .then breaks the chain's error handling promise.then(() => { anotherAsyncCall(); }) — without a return — lets anotherAsyncCall()'s promise run completely detached from the outer chain. If it rejects, no .catch() further down that outer chain will ever see it; it becomes its own separate unhandled rejection.

window.onerror — the other global handler

window.addEventListener("error", (event) => {
  console.error("Uncaught:", event.message, "at", event.filename + ":" + event.lineno);
  event.preventDefault();
});

unhandledrejection above only catches promise rejections nobody caught. This sibling event catches everything else that escapes uncaught — a thrown error inside a plain synchronous function, a syntax error in a dynamically loaded script, an error thrown inside a setTimeout callback. Between the two, nothing that goes wrong in the page is ever completely invisible — which is exactly the mechanism error-reporting services build on to know a production user hit a crash at all.

⚠ It won't catch everything equally well A script loaded from a different origin with no crossorigin attribute reports its errors as a generic, useless "Script error." with no file, line, or message — a deliberate cross-origin privacy restriction, not a bug. Fix it by adding crossorigin="anonymous" to the <script> tag and serving the file with the matching CORS header.

AggregateError — when there's more than one error to report

try {
  await Promise.any([
    fetch("/mirror-1/health"),
    fetch("/mirror-2/health"),
  ]);
} catch (err) {
  console.log(err instanceof AggregateError);   // true — every candidate failed
  console.log(err.errors.length);                // one entry per rejected promise, in order
  err.errors.forEach((e) => console.log(" -", e.message));
}

A normal catch only ever gets one error, because a normal throw only ever carries one. AggregateError — what Promise.any rejects with when every candidate fails — is the built-in shape for "several independent things went wrong at once": it's a real Error (has .message and .stack) that additionally carries the full list in .errors, instead of forcing you to pick just one to report and silently drop the rest.

Immutability — why frameworks care so much

React, Redux, and similar tools decide "did this change?" with a single === check, not a deep comparison — because a deep comparison of a large tree, on every single render, is far too slow to do constantly.

const state1 = { count: 0 };

function mutateInPlace(state) {
  state.count++;
  return state;
}
function updateImmutably(state) {
  return { ...state, count: state.count + 1 };
}

const afterMutate = mutateInPlace(state1);
console.log(state1 === afterMutate);   // what happens?

const state2 = { count: 0 };
const afterUpdate = updateImmutably(state2);
console.log(state2 === afterUpdate);   // what happens?

true, then false. Mutating in place changes the same object — a === check comparing the old reference to the new one sees no difference at all and a framework built on that check will skip re-rendering, even though the data genuinely changed. Building a fresh object every update guarantees a new reference exactly when something actually changed — which is the entire reason "don't mutate state directly" is a rule in React, not just a style preference.

const frozen = Object.freeze({ a: 1 });
frozen.a = 2;             // non-strict script: fails silently, "a" stays 1
                            // strict mode / modules (the normal case today): throws a TypeError
console.log(frozen.a);    // 1 either way — the object never actually changed
Rule Object.freeze is shallow — it locks the object's own top-level properties, but a nested object inside a frozen one is still fully mutable. It's a debugging aid for catching accidental top-level mutation, not a deep-immutability guarantee.

Debugging, past console.log

Tool For
A line-number breakpoint (Sources panel)pause every time execution reaches that exact line
A conditional breakpointright-click the line number — pause only when an expression you type is true, e.g. user.id === 42. Essential once a bug only shows up for one specific input out of thousands.
A watch expressionpin any expression to re-evaluate and display at every pause, without retyping it in the console each time
debugger;a breakpoint written directly in the source — pauses there whenever DevTools is open, no manual click needed
The Network tabevery request's status, timing, headers, and actual response body — the first stop when data "never showed up"

Once paused at any breakpoint, the call stack panel shows the exact chain of calls that got you there — the same information a .stack string gives you after the fact, except you can now inspect every live variable at every level of it, not just read a frozen snapshot of what the values were.

Interview answer, one sentence

"Wrap errors with cause so the original is never lost, remember an async failure only surfaces if something awaits or catches it, and treat window.onerror and unhandledrejection handlers as the last line that reports what slipped through, not as the main strategy."

Before you move on
  • Explain what happens to the original error if you throw a new one without the cause option, and how e.cause fixes it.
  • Explain why a promise rejection with no .catch() anywhere in its chain surfaces as an unhandledrejection event instead of failing silently.
  • Name the specific bug caused by forgetting to return inside a .then(), and why it detaches the inner promise from the outer chain's error handling.
  • Explain why frameworks like React decide "did this change" with a single === check, and why mutating state in place defeats it.
  • Set a conditional breakpoint that only pauses for one specific input, and say why that beats a plain line breakpoint for that case.

02Practice this chapter

Opens in the editor: write it, run it, and check it against real tests.

  • 8 testsChain a caught error into a new oneintermediate
  • 9 testsUpdate state without mutating itintermediate
All 33 for this level →
I7

Finished reading?

0 of 41 chapters read in this series

→Next stop

  1. I6Previous · I6Regex & dates
  2. I7you are here
  3. Next · I8Real-time connectionsfetch answers one question at a time. This is what answers a stream of them.5 min read

01On this page

6 sections · 0%

  1. 01Error chaining with cause
  2. 02Unhandled promise rejections
  3. 03window.onerror — the other global handler
  4. 04AggregateError — when there's more than one error to report
  5. 05Immutability — why frameworks care so much
  6. 06Debugging, past console.log

[] chapters · t top