Phase 4 · Async, Data & ToolingModule 30~40 min read

Errors, Debugging & Source Maps

Handle expected failures and diagnose unexpected ones with developer tools and useful error boundaries.

What you'll learn

Errors are part of an application's contract. Strong JavaScript systems distinguish expected domain failures from defects and preserve enough context to diagnose both.

By the end of this lesson, you'll be able to:

  • Throw and catch at useful boundaries
  • Create domain-specific errors
  • Debug source-mapped production code

Core mental model

Use this decision table as a compact reference. Focus on what each tool means and when it earns its place in production code.

ConceptWhat it meansDecision rule
ExceptionNon-local transfer of failure controlThrow when the current layer cannot fulfill its contract
Custom errorFailure with stable type and structured contextUse when callers need different recovery behavior
Source mapMaps generated code back to authored sourceUpload securely to diagnostics tooling

Professional workflow

Build the behavior in small, observable steps. Each step should leave something you can inspect or test.

  1. Describe the failure and diagnosis boundary boundary: inputs, outputs, state, timing, and expected failures.
  2. Implement the smallest correct path with names that expose intent.
  3. Add edge cases and failure handling before introducing abstractions.
  4. Verify behavior with realistic data and one deliberately adversarial example.
  5. Refactor only after the observable behavior is protected.

Make behavior observable

Before optimizing or abstracting, make inputs, outputs, state changes, timing, and failure paths visible. JavaScript becomes much easier to reason about when hidden work is exposed.

Guided code lab

Preserve a cause chain

The higher layer adds domain meaning without losing the original failure.

errors.js
class CourseLoadError extends Error {
  constructor(courseId, options) {
    super("Could not load course " + courseId, options);
    this.name = "CourseLoadError";
    this.courseId = courseId;
  }
}
async function loadCourse(id) {
  try { return await requestJson("/api/courses/" + id); }
  catch (error) { throw new CourseLoadError(id, { cause: error }); }
}

Recover only where recovery is possible

The UI boundary translates a typed failure into user-facing state.

recovery.js
try {
  const course = await loadCourse("javascript");
  renderCourse(course);
} catch (error) {
  if (error instanceof CourseLoadError) renderRetry(error.courseId);
  else throw error;
}

Production practice

Contract

Document the failure and diagnosis boundary inputs, completion signal, failure channel, ordering, and cancellation behavior.

Verification

Test success, expected failure, timeout or cancellation, empty data, and out-of-order completion.

Operations

Expose duration and failure context without logging secrets or overwhelming the main thread.

Common failure mode

catch (error) followed by an empty block erases the stack, cause, and user-visible evidence while leaving the program in an unknown state.

Independent workshop

Add diagnostic error boundaries to the API dashboard.

Your finished workshop must include:

  • At least two custom error types
  • Preserved error causes
  • A debugging note using breakpoints and a source map

Definition of done

Demonstrate the happy path and at least two edge cases, keep responsibilities separated, and add a short note explaining one design choice.

Recap & quick check

Key takeaways

  • Throw when a contract cannot be met
  • Catch where recovery exists
  • Custom errors encode domain meaning
  • Source maps reconnect generated and authored code

Quick check

1. What does finally guarantee?

2. Why use Error cause?

3. Where should an error be caught?

Keep the workshop. Later modules deliberately build on these decisions, so each exercise can become part of your final portfolio architecture.