Skip to main content

Chapter 5 - Debugging (JavaScript)

Here's a JavaScript-flavoured version of the same concepts, with small JS examples for each idea.


Throwing errors (raising exceptions)

In JavaScript you use throw to signal that something has gone wrong and stop normal execution, usually so a try...catch higher up can handle it.

Example: enforce a positive age.

function setAge(age) {
if (age <= 0) {
throw new Error("Age must be positive.");
}
console.log("Age set to", age);
}

setAge(25); // OK
setAge(0); // Throws Error: Age must be positive.

You'd normally catch this where you call setAge:

try {
setAge(0);
} catch (err) {
console.log("Problem setting age:", err.message);
}

Simple "assertions"

JavaScript has no built-in assert keyword, but you can create a tiny helper that throws if a condition is false.

Example: basic assert.

function assert(condition, message) {
if (!condition) {
throw new Error(message || "Assertion failed");
}
}

const numbers = [1, 3, 5, 7];
numbers.sort((a, b) => a - b);

assert(numbers[0] <= numbers[numbers.length - 1], "List is not sorted");

If you accidentally reverse instead:

const nums = [1, 3, 5, 7];
nums.reverse();
assert(nums[0] <= nums[nums.length - 1], "List is not sorted"); // throws

Use this style of assertion for programmer errors during development, not for user input validation.


Logging with console

Instead of Python's logging module, JavaScript typically uses console.log, console.debug, console.info, console.warn, and console.error for logging.

Example: simple logging.

console.debug("Start of program");
const x = 5;
console.info("x is", x);
console.warn("This is a warning");
console.error("Something went wrong");

Browsers let you filter by level (e.g., only show warnings and errors) in DevTools.


Using logs to debug logic

You can sprinkle logs inside loops or functions to see how variables change and locate bugs, similar to Python's factorial example.

Example: factorial with a bug and logs.

function factorial(n) {
console.debug(`Start factorial(${n})`);
let total = 1;
for (let i = 0; i <= n; i++) { // Bug: starts at 0
total *= i;
console.debug(`i=${i}, total=${total}`);
}
console.debug(`End factorial(${n})`);
return total;
}

console.log(factorial(5)); // 0, debug logs show i starts at 0

Fix:

for (let i = 1; i <= n; i++) {
total *= i;
}

Logging to a file (Node.js)

In Node.js, you can write logs to a file by appending to it, similar in spirit to Python's logfiles.

Example: minimal Node.js file logger.

// logger.js
const fs = require("fs");
const path = require("path");

function log(message) {
const line = `${new Date().toISOString()} - ${message}\n`;
fs.appendFileSync(path.join(__dirname, "app.log"), line);
}

log("Program started");
log("Doing some work...");

In the browser, logging to a file directly isn't standard; you typically log to the console and, if needed, send logs to a server.


Logging levels pattern

JavaScript doesn't enforce logging levels like Python's logging module, but you can approximate them with wrappers that respect a configured minimum level.

Example: simple levelled logger (browser/Node):

const LEVELS = { DEBUG: 10, INFO: 20, WARN: 30, ERROR: 40 };
let currentLevel = LEVELS.DEBUG;

const log = {
debug: (...args) => {
if (currentLevel <= LEVELS.DEBUG) console.debug(...args);
},
info: (...args) => {
if (currentLevel <= LEVELS.INFO) console.info(...args);
},
warn: (...args) => {
if (currentLevel <= LEVELS.WARN) console.warn(...args);
},
error: (...args) => {
if (currentLevel <= LEVELS.ERROR) console.error(...args);
}
};

log.debug("Some detailed info"); // shown
currentLevel = LEVELS.ERROR;
log.info("Will be hidden now"); // not shown
log.error("Something broke"); // shown

This mirrors the idea of DEBUG/INFO/WARNING/ERROR levels.


Disabling logging

You can "disable" logs by swapping console methods or your own logger methods with no-ops in production builds.

Example: turn off debug/info logs:

if (process.env.NODE_ENV === "production") {
console.debug = () => {};
console.info = () => {};
}

Or with the custom logger above, just bump currentLevel so only errors show.


Using the debugger

All modern browsers and Node.js support interactive debugging where you run code line by line, inspect variables, and step into/over functions, just like Mu's debugger.

Basic pattern:

  1. Open DevTools (e.g., Chrome: F12 or Ctrl+Shift+I).
  2. Go to Sources (or Debugger) tab.
  3. Load your script, set a breakpoint, then reload the page.

Example script:

console.log("Enter two numbers in the prompt boxes.");

const first = prompt("First number:");
const second = prompt("Second number:");

console.log("The sum is " + first + second);
// Use debugger tools to see that first/second are strings

In DevTools, you can step through each line, watching the values of first and second, then decide to convert them:

const sum = Number(first) + Number(second);
console.log("The sum is", sum);

Breakpoints

A breakpoint is a marker where execution pauses so you can inspect state, instead of stepping from the start every time.

Ways to set breakpoints:

  • Click the line number in the DevTools Sources/Debugger panel.
  • Or add the debugger; statement in code; when DevTools is open, execution stops there.

Example: coin flip counter with a breakpoint.

let heads = 0;

for (let i = 1; i <= 1000; i++) {
if (Math.random() < 0.5) {
heads++;
}
if (i === 500) {
debugger; // Execution pauses here if DevTools is open
console.log("Halfway done!");
}
}

console.log("Heads came up", heads, "times.");

At the breakpoint, you can inspect i and heads, then continue.


Putting it together

In JavaScript, you combine thrown errors, small assertion helpers, structured logging (via console or your own wrapper), and the DevTools/Node debugger with breakpoints to track down and understand bugs efficiently, mirroring the Python techniques from the chapter.