Chapter 18 of 28 · JavaScript for Beginners — Complete Free Tutorial
Error Handling & Debugging
Errors are not the end of the world — they are information. JavaScript gives you structured error types, try/catch for recovery, throw for enforcing rules, and browser dev tools powerful enough to find any bug. This chapter teaches both halves: surviving errors and hunting them.
What an error looks like
try {
null.name;
} catch (err) {
console.log(err.name); // → TypeError
console.log(err.message); // → Cannot read properties of null
console.log(err.stack); // → call stack leading to the failing line
}
Uncaught errors stop the rest of that script's execution (later scripts still run) and appear in the console in red.
The common error types
| Type | Typical cause | Fix |
|---|---|---|
ReferenceError | Variable not defined (typo, TDZ) | Declare it / fix the name / check scope |
TypeError | Wrong type: null.x, notAFn() | Guard with ?., verify types |
SyntaxError | Broken syntax — script does not run at all | Missing brace/quote; check the reported line |
RangeError | Value out of range (e.g., stack overflow from recursion) | Add base case / bounds |
URIError | Malformed decodeURIComponent input | Validate before decoding |
try / catch / finally
function divide(a, b) {
if (b === 0) throw new RangeError('Division by zero');
return a / b;
}
try {
console.log(divide(10, 2)); // → 5
console.log(divide(10, 0)); // throws → jumps to catch
} catch (err) {
console.error(`Failed: ${err.message}`); // → Failed: Division by zero
} finally {
console.log('always runs — cleanup here'); // even after catch/return
}
finally is where you close spinners, release locks or hide loading states — it runs whether things succeeded or not.
throw: your own errors
class ValidationError extends Error {
constructor(field, message) {
super(message);
this.name = 'ValidationError';
this.field = field;
}
}
function setAge(n) {
if (n < 0 || n > 150) throw new ValidationError('age', 'Age out of range');
return n;
}
try { setAge(300); }
catch (e) {
if (e instanceof ValidationError) showFieldError(e.field, e.message);
else throw e; // unexpected errors: never swallow them
}
Custom errors carry context (field) so callers can react precisely. Rule: catch what you can handle; rethrow what you cannot.
Error handling in async code (preview)
// Promises: .catch or rejected promise in try/catch with await
fetch('/api/users')
.then((r) => r.json())
.catch((err) => console.error('network:', err));
async function load() {
try {
const res = await fetch('/api/users');
const data = await res.json();
} catch (err) {
// handles both fetch failures AND json() failures
}
}
Full coverage in Promises and Async/Await.
Debugging toolkit
console — more than log
console.log('plain');
console.warn('suspicious');
console.error('broken');
console.table(users); // grid view of arrays/objects
console.group('Checkout');
console.log('step 1');
console.groupEnd();
console.time('loop');
// …work…
console.timeEnd('loop'); // → loop: 3.2ms
console.assert(items.length > 0, 'cart empty');
Breakpoints (dev tools → Sources)
- Click a line number in your script to set a breakpoint; run the code — execution pauses there.
- Step over (
F10), step into (F11), step out (Shift+F11). - Inspect the Scope panel: every local, closure and global variable at that instant.
- Add a Conditional breakpoint (right-click) to stop only on the 47th iteration.
- “Pause on uncaught exceptions” finds where a red error originates.
Other essential panels
- Network — did the request go? What status/body came back?
- Elements — is the DOM what you think it is? What CSS wins?
- Console utilities —
$_(last result),$0…$4(selected node),$$('#id .x')(querySelectorAll shorthand).
A debugging method that works
- Read the error line number — 80% of bugs end right there.
- Reproduce reliably — if you cannot trigger it, you cannot fix it.
- Bisect — comment out halves of the flow to isolate the failing stage.
- Inspect actual vs expected — log the value one step earlier than you suspect.
- Fix, then add a guard or test so it stays fixed.
Defensive habits
const city = order?.address?.city ?? 'Unknown'; // optional chaining + fallback
const items = Array.isArray(data.items) ? data.items : [];
const num = Number.isFinite(value) ? value : 0;
Common mistakes
- Empty
catch {}— bugs vanish silently; at minimumconsole.error(err). - Swallowing unexpected errors — catch specific ones; rethrow the rest.
- Debugging with
alert()— blocking and destructive (changes focus); useconsoleor breakpoints. - Ignoring the first error — cascading failures follow the original; fix top-down.
- No error state in UI — if
fetchfails and nothing tells the user, the app looks broken; always render failure states.
Practice checklist
- Wrap risky JSON parsing in
try/catchwith a friendly fallback message. - Create a custom
ApiErrorclass with astatusfield and throw it on bad responses. - Set a breakpoint in a loop, inspect the scope at iteration 5, and find an off-by-one bug you plant on purpose.
- Use
console.tableon an array of objects and compare it to plainlog.
You can now read errors — next, understand the machinery that makes timing tricky: Asynchronous JavaScript & the Event Loop.
Frequently Asked Questions
JavaScript is the programming language of the web. It adds behaviour to pages: responding to clicks and input, validating forms, fetching data from APIs, updating the DOM without reloading, and much more. It also runs on servers (Node.js), in desktop and mobile apps, and in build tooling.
They are unrelated languages that share a similar name for historical marketing reasons. Java is a statically-typed compiled language; JavaScript is a dynamically-typed interpreted language designed for the browser. Knowing one does not imply knowledge of the other.
Helpful but not mandatory — HTML gives you the structure JavaScript manipulates and CSS the styling it toggles. If you know basic tags, classes and selectors, you have enough to start; the DOM chapters assume only that much.
Use const by default and let when the value must change. Avoid var entirely in modern code: it is function-scoped instead of block-scoped, hoists differently, and can create accidental globals. This course uses const and let throughout.
The Document Object Model is the browser’s in-memory tree of objects representing your HTML. JavaScript reads and modifies that tree — selecting elements, changing text, adding nodes, attaching event listeners — and the browser re-renders the page to match.
== compares after converting types (so 5 == "5" is true), while === requires the same value AND the same type (5 === "5" is false). Loose equality has famously confusing rules — use === and !== everywhere unless you have a specific reason not to.
JavaScript is single-threaded, so long-running work (timers, fetches, events) is handled by the browser and its callbacks are queued. The event loop moves queued callbacks onto the call stack only when it is empty, with promise callbacks (microtasks) taking priority over timers. It is why asynchronous code never freezes the page.
A Promise is an object representing a future value — pending, fulfilled or rejected — that can be chained and combined (Promise.all). async/await is syntax sugar over promises that lets asynchronous code read top-to-bottom with ordinary try/catch error handling.
Yes. Frameworks are built on exactly these fundamentals — components are functions, state is your variable discipline, effects are events, rendering is DOM work. Developers who learn vanilla first understand what the framework is doing and debug far faster.
Type out the examples instead of copying, build small end-to-end projects (expense tracker, weather app, quiz), handle every UI state (loading, empty, success, error), and revisit fundamentals while reading real code. Consistency beats volume — a little most days outpaces occasional marathons.