Skip to the notes
JSGroundwork
JSGroundwork handwritten · web dev
✎Playground→⌘Problems↻Review🔥Progress

Chapters

27 chapters
⌕
Beginner9›
B1Setup & mental modelB2Types & valuesB3Operators & flowB4Functions (first half)B5Objects & arrays (first half)B6DOM & eventsB7Basic asyncB8Errors & tools★Cheat page
Intermediate10›
I1Scope & functions, properlyI2Objects deeplyI3Prototypes & OOPI4Async, properlyI5Modules & toolingI6Regex, dates & APIsI7Error handlingI8Real-time connectionsI9Offline & storage★Cheat page
Advanced10›
A1Engine & memoryA2Advanced asyncA3MetaprogrammingA4Types & dataA5Patterns & architectureA6PerformanceA7SecurityA8EcosystemA9Testing★Cheat page
/ search[ ] chaptert top

JavaScript levels

1Beginner2Intermediate3Advanced

Ready to read

JSJavaScript⑂Git◎Interview prepΣDSA in JSSDSystem Design
More topics15›
</>HTML{ }CSS⚛ReactNNext.jsNeNest.jsTSTypeScriptNoNode.js🐳DockerDBSQL & Databases✓Testing🔒Web Security☁Cloud & DevOps◈GraphQL◆Redis☸Kubernetes
100%
I7

Error handling & debugging

The Intermediate track's close-out — past what B8 already covered.

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.

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.

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.

Practice this layer

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

Chain a caught error into a new one2 tests · intermediateUpdate state without mutating it3 tests · intermediate
←previousRegex, dates & APIs↑ CovernextReal-time connections→