Errors, Testing & Tooling

try/catch, custom errors, Vitest/Jest basics, ESLint, Prettier, and debugging.

Production JavaScript needs predictable failures and automated checks — not console.log archaeology alone.

Core concepts

  1. Exceptions: try/catch/finally; throw Error subclasses with messages and optional cause. — MDN: Exception handling
  2. Async errors: Unhandled Promise rejections crash Node processes — always .catch or try/await. — MDN: Promise error handling
  3. Unit tests: Vitest/Jest — arrange/act/assert; mock fetch and modules at boundaries. — Vitest guide
  4. Lint & format: ESLint catches bugs; Prettier handles style — run in CI. — ESLint getting started
  5. Type checking optional layer: TypeScript or JSDoc @typedef for larger repos. — TypeScript handbook
javascript
async function loadUser(id) {
  const res = await fetch(`/api/users/${id}`);
  if (!res.ok) throw new Error(`HTTP ${res.status}`);
  return res.json();
}

Concept videos

YouTube lessons for these topics. On a large screen they open in the reading panel next to the roadmap, same as the article links on each core concept.

Explanation

Shipping code is not about never failing — it is about failing loudly, early, and recoverably. Exceptions, tests, and linters are three layers of safety net under the trapeze artist.

Exceptions

try/catch wraps risky operations: parsing JSON, reading files, calling payment APIs. Throw Error objects with messages humans can read; add cause when wrapping lower-level failures. finally runs cleanup (close files, hide spinners) whether or not an error occurred.

Async errors

A rejected promise without .catch is an unhandled rejection — Node may exit; browsers log scary red errors. Always end async chains with handling or use try/catch around await. Async stack traces are harder to read; good error messages save hours.

Unit tests

Tests are executable specifications. Arrange inputs, act on the function, assert outputs. Mock external HTTP at boundaries so tests stay fast and deterministic. One failing test is cheaper than one angry user tweet.

Lint & format

ESLint finds likely bugs (no-unused-vars, missing hooks dependencies). Prettier stops bike-shedding about semicolons. Running both in CI means reviews focus on logic, not spacing.

Type checking optional layer

JavaScript stays dynamic at runtime; TypeScript or JSDoc adds guardrails at edit time. You do not need types on day one, but large teams use them to document contracts between modules and catch refactors before deploy.

What Is It?

JavaScript throws Error objects (or subclasses) that bubble until something catches them.

Testing frameworks like Vitest/Jest run assertions in CI so refactors do not silently break behavior.

ESLint catches footguns; Prettier formats; together they keep teams aligned without style wars.

Why Should I Care?

Untested async code fails in production at 2 a.m. Tests document expected behavior better than comments.

Consistent tooling is why open source contributions feel familiar across repos.

Real World Analogy

Tests are rehearsal before opening night; linters are the stage manager who stops you from tripping on loose cables before the audience arrives.

Visual Mental Model

text
code --> eslint/prettier --> vitest/jest --> CI green/red

Beginner Explanation

Throw meaningful errors, catch at boundaries, and write small unit tests for pure functions. Let Prettier fight formatting debates so you can review logic.

Smallest Possible Example

javascript
function divide(a, b) {
  if (b === 0) throw new Error("zero divisor");
  return a / b;
}
try { divide(1, 0); } catch (e) { console.error(e.message); }

Common Mistakes

❌ Empty catch (e) {} blocks.

You swallow failures and lose stack traces. Log, rethrow, or map to a user-safe error.

❌ Testing implementation details instead of behavior.

Brittle tests break on refactors that keep UX the same. Assert outputs and side effects users care about.

❌ Skipping lint in CI 'to move fast'.

You move fast into the same bugs linters were invented to catch.

Real Project Example

Vitest powers many Vite + Vue/React projects; Jest still dominates older React codebases.

ESLint default configs ship with Next.js and Create React App scaffolds.

Build This Mini Project

Add Vitest to a tiny module, write three tests (happy path, error path, edge case), and wire npm test in package.json.

Interview Question

When would you use try/catch vs Promise .catch?

Use try/catch around synchronous code and inside async functions with await. For Promise chains, .catch at the end or try/catch in async functions both work — pick one style per function and always handle rejections.

Key Takeaways

  • Fail loudly with useful error messages.
  • Automate tests for core logic.
  • Lint and format in CI, not optionally.
  • Debug with breakpoints, not infinite console.log.

Resources

The 60-second story

A small bakery writes the specials on a chalkboard. Some mornings the chalk breaks. That is an exception: something in the process failed, and you want a chance to catch it before the customer watches you stare at the wall. try is attempting the special. catch is the backup plan. finally is wiping the counter whether the croissant survived or not. Throw an Error with a real message. “Something went wrong” is a blank chalkboard.

Async makes this sneakier. A burned batch that starts after you walked away is a rejected Promise. If nobody is listening, Node can treat that like a fire alarm nobody hung. Always await inside try or attach .catch. Unhandled rejections are not a style preference. They are how servers die at 2 a.m.

Tests are the friend who orders the usual before you open. Arrange the ingredients, act by calling the function, assert that the bag contains a croissant and not a shoe. Vitest and Jest are that friend on a schedule. Mock fetch at the boundary so the test does not depend on a real oven across town.

Map it.

  • try / catch / finally = attempt, backup plan, cleanup.
  • A rejected Promise with no listener = a fire alarm in an empty room.
  • A unit test = a regular who knows what “correct” tastes like.
  • ESLint = the coworker who says “that variable is not defined” before customers arrive.
  • Prettier = the person who lines up the chairs so review is not a debate about spacing.
  • TypeScript or JSDoc = labels on the bins so you stop pouring salt into the sugar.
javascript
async function loadMenu(url) {
  try {
    const response = await fetch(url);
    if (!response.ok) throw new Error(`menu ${response.status}`);
    return await response.json();
  } catch (error) {
    console.error(error);
    return [];
  }
}

The backup plan returns an empty menu instead of crashing the page. That is a product choice. The point of catch is that you make the choice on purpose.

Linters were invented because humans reread code worse than they think. A machine that flags === mistakes and unused names is cheaper than a production incident. Formatters were invented so pull requests stop arguing about commas. Types showed up in larger bakeries because “the menu item is probably a string” stops being cute when twelve people edit the same oven.

Mental model: a fire alarm, a regular customer, and labeled bins. Errors are the alarm. Tests are the regular. Lint and types are the labels that keep salt out of the sugar.

You have this page if you can say: catch specific failures, never ignore a rejected Promise, test the boundary not the whole city, and let tools argue about style so humans can argue about behavior.

Threads in the same domain as this chapter — go deeper on iHateReading without leaving the roadmap.

Bench.js cover

Bench.js

Micro-benchmark mindset for JavaScript performance experiments.

Why websites are slow — bundle size cover

Why websites are slow — bundle size

Parse and compile cost before you blame images alone.