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.