Chapter 4 of 18

Reading and debugging

How to read code you did not write, and how to find a bug when the agent has run out of ideas.

You won't write much code from a blank page any more. You will read a great deal of it, and you can't supervise what you can't read. Nobody needs to be fluent for that. You need to look at a function, a named piece of code, and say what goes in, what comes out, and what else it touches, the way you can follow a contract without being a lawyer.

There are only about a dozen ideas in the code you will meet, and the first half of this chapter shows each one by running it. The instruments here step through real programs a line at a time, so you can watch what a variable holds, which function is waiting for which, and where an error goes when it is thrown.

The second half is debugging, which is a procedure and not a talent: make the bug happen on demand, cut the search in half until it has nowhere left to hide, and fix the cause rather than the symptom. Agents are quick at writing code, and they can get stuck for a long time on a bug they misunderstood. When that happens, this is what you do with the agent turned off, and it is what an interviewer means when they ask if you can debug without one.

Reading code

Working out what code does by following it. You rarely read a file from top to bottom. You start where the program starts, or where the problem showed up, then follow the calls, each place one piece of code starts another, and ignore everything that path never touches.

An agent can write more in a minute than you can read in an hour, so reading all of it is off the table. What you can do is follow one path through a change and ask about any step you can't explain. That is supervision, and it is what an interviewer is testing when they point at a file and say walk me through it.

Say it to your agent

“Before you change anything, trace what happens when POST /api/checkout is called. List each function in the order it runs, with its file and line, and stop at the first database call. I'll read along.”

Now the building blocks, in the order you meet them. Each one is easiest to understand by watching it run, so press Step on the instruments below and see what the program is holding at each line.

Variable

A name for a value the program is holding, like a label on a box. In JavaScript, let means the name can be given a new value later, and const means it can't.

Most bugs come down to a variable not holding what someone assumed it held. When you read an agent's code, the question to keep asking is: what is in this name, right now, on this line?

Say it to your agent

“This function reassigns result five times. Give each step its own const, with a name that says what it holds, and change no behaviour. Show me the tests still pass.”

Function

A named piece of code you run by writing its name followed by brackets. It lets one idea be written once and used in many places, and it lets a reader take in the name rather than the details.

An agent that hasn't seen the function you already have will happily write a second one. Now one rule lives in two places, and the next fix only reaches one of them. So it pays to ask whether something exists before anything gets added.

Say it to your agent

“Before adding a helper, run rg -n "formatMoney|formatPrice" src/ and tell me what exists. Use it if it fits. If you add a new one, put it in src/lib/money.ts and say why the old one didn't fit.”

Calling a function pauses the code that called it. The panel on the right of the next instrument lists what is running, newest first. That list is called the call stack, and it will matter later.

Parameters and return values

Also called Arguments

Parameters are the inputs a function asks for, named in its brackets. The values you pass in are the arguments. return hands one value back to the caller and ends the function, and a function with no return hands back undefined, JavaScript's word for nothing.

A function's parameters and return value are its promise to the rest of the code. When an agent changes them, every caller is affected, including the ones it didn't read.

Say it to your agent

“You changed the parameters of createOrder. Run rg -n "createOrder\(" src/ and list every call site, then confirm each one passes the new arguments in the right order. Show me the list.”

Type

What kind of value something is: a number, some text (called a string), true or false (a boolean), and so on. The type decides what an operation means, so + adds two numbers but glues two strings together.

Whatever arrives from a form or an API is often text, even when it looks like a number. In plain JavaScript that gives a wrong answer with no error. TypeScript, which is JavaScript with types written in, turns it into an error before the code runs, and an agent can read that error and fix it by itself.

Say it to your agent

“The price comes from a form, so it is a string. Convert it once, where it enters, using Number(). Reject NaN with a clear error, and type it as number everywhere after that. No any.”

Conditions and loops

Also called If statement, For loop

A condition (if) runs some code only when a test is true. A loop (for, while) repeats some code until its test turns false. Between them, they decide which lines run and how many times.

Reading an agent's code, every if is a question: what happens in the other case? A branch nobody wrote and a loop that stops a step early are where a lot of almost-right code hides.

Say it to your agent

“This function has four nested ifs. Rewrite it with early returns so the main path is not indented, keep the behaviour identical, and add a test for each branch, including the one with no else.”

Lists and objects

Also called Array, Dictionary

A list, called an array, holds values in order, numbered from 0. An object holds values under names, like a small form. Both live in a box in memory, and a variable holds a pointer to the box, not what is in it.

That pointer is behind a whole family of bugs where you change a "copy" and the original changes too. Spot them by asking if the code made a new box or only a new name. JSON, from the network chapter, is these same two shapes written out as text.

Say it to your agent

“Don't mutate the cart argument in applyDiscount. Return a new object with a new items array, and add a test that checks the original cart is unchanged after the call.”

Modules and imports

A module is a file that shares some of what it defines using export. Another file pulls those parts in using import. Anything not exported stays private, so a module's insides can change without breaking the files that use it.

The import lines at the top are the quickest map of a file: they say what it depends on. They are also where agents go wrong in a way you can check, by importing a function that doesn't exist or a package nobody ever published.

Say it to your agent

“Run bun run typecheck after adding imports. For every new package in package.json, show me its page on npmjs.com and how many weekly downloads it has before you install it.”

The next few are about what happens when the program meets something it did not expect.

Null and undefined

Two ways JavaScript says "nothing here". undefined usually means nothing was ever set, and null usually means someone set it to nothing on purpose. Reading a property of either one crashes the program, with an error you will see often.

Agents like ?., a shortcut that skips the read when the value is missing, because it makes the crash go away. But it swaps an error you would notice for a wrong answer you might not. The real question is what should happen when the value is missing, so ask that, and then have it written down in the code.

Say it to your agent

“user.address can be missing for guest checkouts. Don't add ?. to silence it. Show a "no address yet" message on the page, and add a test with a guest user that checks that message appears.”

Exception

Also called Thrown error

An error that stops the current function and climbs back through whoever called it, until some code catches it using try and catch. If nothing does, the program ends.

An empty catch hides the failure from you and from every automatic check, which makes it a very easy way to turn a failure into a pass. Question any catch that does not log, retry, rethrow or tell someone.

Say it to your agent

“Search src/ for catch blocks that do nothing. For each one, rethrow it, log it with the request id, or explain to me why ignoring it is right. Then add a lint rule that bans empty catch blocks.”

Stack trace

Also called Backtrace, Traceback

What a crash leaves behind: the error message, then every function that was running at that moment, newest first, each with a file and a line. It is a map from where it blew up back to where it started.

It is the most precise bug report you will ever get, and it's free. Give an agent the whole thing, not your summary of it. Then check its story against the trace: if it blames a function that isn't there, it is guessing.

Say it to your agent

“Here is the full stack trace. Start from the top frame that is in our code, open that file at that line, and tell me which value was wrong there and where it came from, before you propose any fix.”

Classes and objects

A class is a blueprint. Each object made from it with new gets its own data and shares the functions, called methods. Inside a method, the word this means the object it was called on.

Classes gather data and the rules for changing it in one place, and you will read plenty even if you never write one. When an agent's class grows long, ask what it holds and what changes it, because a class with many reasons to change is hard to review.

Say it to your agent

“This class has 14 methods and 9 fields. List which methods use which fields, and propose how to split it so each part has one reason to change. Do not refactor anything yet.”

Everything so far happened one line at a time, in order. As soon as a program has to wait for something, that stops being true.

Async and await

Also called Promise

Slow work, such as a network call, doesn't hold everything up. The program gets a promise of a result and moves on. await pauses one function until its promise finishes, and other code gets to run in the meantime.

A missing await is a classic almost-right bug: the code runs, nothing errors, and it reports success before the work has happened. Look for it in anything an agent writes that saves, sends or charges.

Say it to your agent

“Turn on the no-floating-promises rule from typescript-eslint, fix everything it reports, and list each place where a promise was not being awaited, saying what could have gone wrong there.”

The bugs that take longest to find are often not in any one line. They are in what the program remembers.

State

Everything a program remembers at a given moment: its variables, what is in its database, what you have selected. The same function can behave differently depending on state, so a bug can depend on what happened before, not only on what you did just now.

Agents read code, and state is mostly not in the code. It only exists while the program runs, which is why an agent can stare at correct-looking code and find nothing. So hand it the sequence of steps, not only the symptom.

Say it to your agent

“This only fails on a second checkout in the same session. Write a test that does add, pay, new cart, add, pay in that order and checks the customer is charged twice. It should fail. Then fix it.”

Side effect

Anything a function does beyond handing back its answer: writing to a database, sending an email, changing something outside itself, even printing. A program's useful work is mostly side effects, and so is most of its risk.

An agent retrying a failed step reruns its side effects, so the email goes out twice and the card is charged twice. When you read its code, find the lines that change the outside world. Those are the ones you can't take back.

Say it to your agent

“Keep the calculation and the charge apart. Put the total in a plain function with no I/O and test that, and make chargeCard the only place that talks to Stripe, using an idempotency key.”

Pure function

A function whose answer depends only on its inputs, and which changes nothing else. Same input, same output, every time. Code that is not pure is called impure, which is no insult: impure code does the useful work.

Pure functions are the easiest code to review, because you can understand one without knowing about the rest of the program. Asking an agent to push its logic into them is one of the cheapest ways of getting code you can check.

Say it to your agent

“Split out calculateDiscount(cart, coupon) as a pure function: no database, no clock, no global state. Pass today's date in as a parameter. Test it with a table of inputs and expected outputs.”

Scope and closures

Scope is which names a line of code can see. A name made inside a function or a block can only be seen there. A closure is a function that keeps hold of the variables around where it was created, even after that code has finished.

Agents write functions that get called later all the time, and closures explain a whole family of bugs where one of them uses an old value. If a button acts on out-of-date data, this is the first thing to suspect.

Say it to your agent

“This click handler logs an old count. I think its closure captured the value from an earlier render. Confirm that with a log first, then fix it using setCount((c) => c + 1). Do not add a ref.”

That is enough to read most of what you'll meet. Now for finding things out, starting with the oldest tool there is.

Logging

Also called Print debugging

Making the program write down what it is doing as it runs, with something like console.log. It lets you see inside a run without stopping it, and what comes back is evidence, not a theory about what happened.

Logging suits agents better than almost any other debugging tool, because an agent works by running a command and reading the output. When one starts theorising about a bug, tell it to add logs and report what they said. Then make sure it takes them out.

Say it to your agent

“Don't guess yet. Add a labelled console.log at each step of calculateTotal, printing the values. Run it on the failing input, paste the output, and tell me the first line where a value is wrong. Then remove the logs.”

Debugger

A tool that runs your program under your control. It pauses on a line you chose, shows you every variable and the call stack, and moves on one step at a time. Browsers have one built in, and so do most code editors.

Coding agents mostly work by running commands and reading what comes back, so pausing a live program and looking around is awkward for them. For a person it is one of the fastest ways of finding out what is really true, and being able to drive one with the agent off is worth showing.

Say it to your agent

“Give me the exact command to run this test under the debugger, paused at the start, using node --inspect-brk. I will step through parseOrder myself and tell you what I find.”

Breakpoint

A mark on a line that tells the debugger to pause there, so the program can run at full speed up to the part you care about. A conditional breakpoint only pauses when a test you wrote is true.

You can't press Step sixty thousand times. A condition such as price < 0 makes the machine do the searching. Working out what would only be true at the moment things go wrong is most of the skill, and that same question makes your instructions to an agent sharper.

Say it to your agent

“I paused at line 88 and order.total is already wrong there, but the inputs were right at line 40. Read lines 40 to 88 and list every place that changes total.”

Debugging has the same shape every time, and it starts before anyone looks at the code.

Reproduction

Also called Repro, Steps to reproduce

A set of steps that makes the bug happen, ideally every time. Without one, you can't tell if a change fixed the bug or if it just didn't happen this time. A bug you can't reproduce isn't fixed, only quiet.

Agents like to jump straight to a fix, and an agent that never saw the failure can report success with nothing behind it. Make it produce the failure first, and show you. The best form of this is a failing test.

Say it to your agent

“Reproduce this before you touch the code. Write a failing test in tests/checkout.test.ts using this exact input, run it, and paste the red output. Then fix it and paste the green output.”

Minimal reproduction

Also called Reduced test case, Delta debugging

The smallest input or program that still shows the bug. You get there by removing things until the failure goes away, then putting the last thing back. What remains is often close to an explanation.

A small reproduction is cheap to hand to an agent and leaves it nothing irrelevant to chase, so you get a better answer for less.

Say it to your agent

“This 4,000 row CSV crashes the import. Write a script that deletes rows and reruns the import until it has the fewest rows that still crash, and save them as tests/fixtures/import-crash.csv.”

Hypothesis

One specific guess about the cause that a single check could prove wrong, such as "the price is already a string by line 40". Debugging goes fastest when you test one at a time and change one thing at a time.

If an agent changes five things and the bug goes away, nobody knows which one did it, and the other four stay in your code as superstition. Ask for the guess and the check before any change, because a guess you can disprove in one command is cheap.

Say it to your agent

“Give me three hypotheses for why this fails, each with one command or log that would rule it out. Run those checks and report what they showed. Change no code until one has survived.”

Debugging by halving

Also called Binary search

Finding a fault by checking the middle. If things are still right there, the bug is in the later half, and if not, the earlier half. Each check rules out half of what is left, so a thousand places takes about ten checks.

It needs no understanding of the code, only a way of telling good from bad, so it still works when you are lost and the agent is too. Checking in order is the natural thing to try, and the slow one.

Say it to your agent

“Run git bisect between v1.4 and HEAD, using git bisect run bun test tests/checkout.test.ts as the check. Tell me which commit it finds and show me its diff.”

Explain it out loud

Also called Rubber duck debugging

Explaining the problem line by line to anything at all: a colleague, a pet, a rubber duck. You often spot the mistake part way through, because explaining makes you say what you had only been assuming.

Writing a careful bug report for an agent does the same job, and you will often find the answer before you send it. It works the other way too: making an agent explain code back to you shows what it assumed, though its account of its own reasons can be a made-up story.

Say it to your agent

“Don't fix anything yet. Explain this function to me line by line, saying what you expect each variable to hold at that point. Mark any line you are not sure about with UNSURE.”

Finding where it hurts is not the same as finding out why.

Root cause

Also called Symptom

The symptom is what you can see going wrong. The root cause is the earliest wrong thing that produced it, and the place where a fix ends it for good. A fix at the symptom only hides it, and it comes back somewhere else.

An agent told to "fix the NaN" will do exactly that, possibly by hiding the NaN, and the test it writes will agree. Keep asking why the value was wrong until you reach the place it was created.

Say it to your agent

“You removed the NaN, but why was it NaN? Trace the value back from the screen to where it first went wrong, tell me the file and line, and fix it there. Take out the || 0.”

Some bugs are common enough to know on sight. This one is probably the best known.

Off-by-one

Also called Fencepost error

Being wrong by exactly one at an edge: stopping a step too early or too late, or miscounting because lists number from 0. A list of 5 has positions 0 to 4, so position 5 is off the end.

These get past casual checking, because the middle of the data is fine and only the edges break: the first item, the last item, the empty list. Agents make them too. So ask for tests at the edges, because reading the code carefully is not a reliable way of catching one.

Say it to your agent

“Add tests for getPage at the edges: page 1, the last page, one past the last page, an empty list, and a list with exactly one full page. Run them before you change the code.”

Heisenbug

A bug that goes away or changes when you try to look at it, because looking changes the timing or the conditions. The name is a joke on Heisenberg's uncertainty principle.

Logs and debuggers change timing, so "I added a log and now it works" is a finding, not a fix. An agent can easily report that run as a success. Look for something that depends on timing, which usually means two things sharing state.

Say it to your agent

“The test passes with the log and fails without it, so this is timing. Don't keep the log. Find which two things run at once and share balance, write a test that forces the bad order, and then fix it.”

Three last tools for finding your way around code, starting with the one already in the file.

Comments

Text in a file meant for people, which the program ignores. In JavaScript it starts with //. The useful ones say why the code is the way it is, which the code itself can't tell you.

Because the program ignores comments, nothing keeps them honest, and agents read them as fact. A stale comment misleads the next agent as well as you. Cut the ones that only repeat the line below, and keep the reasons.

Say it to your agent

“Delete comments that only restate the code. Keep one wherever a decision would surprise a reader, and write why: for example, // capped at 3 retries because the provider limits us to 5 calls a second. Change nothing else.”

Grep and code search

Also called ripgrep, Regular expression

Searching every file for some text or a pattern. grep is the old command, and ripgrep (rg) a fast modern one. A regular expression is a small language for patterns: \bprice\b finds the word price, but not priced.

It is how agents find their way around a codebase, and how you check what they tell you. "Nothing else calls this" is a claim you can test with one command, so test it.

Say it to your agent

“Before you delete formatPrice, run rg -n "formatPrice" . and paste the output. Also look for names built at runtime, such as "format" + name. Then tell me why it is safe.”

Language server

Also called Go to definition, LSP

A program running beside your editor that understands your code's structure. It powers go to definition, find references, rename and the red underlines. Because it follows scope and imports, it knows which of five things called total you mean.

Search shows every place a word occurs. A language server shows the places that mean this thing, and that difference matters when something gets renamed. Agents can use one as well, and it is often worth setting up for them.

Say it to your agent

“Rename calculateTotal to orderTotal using the language server's rename, not find and replace. Then run bun run typecheck and show me that it is clean.”

All of which leads to the moment this chapter exists for.

Fix loop

Also called Going round in circles

When an agent tries fix after fix for the same bug, each one plausible, none of them working, some undoing the last. More attempts rarely help when its idea of the problem is wrong. This is the point at which you step in.

Stop it, go back to the last good commit, get a reproduction, halve the search, and hand back something specific you have observed, not "still broken". This is what "I can debug without the agent" means in practice, and it is a skill employers now ask about directly.

Say it to your agent

“Stop changing code. Three fixes have failed, so your idea of the cause is wrong. Go back to the last commit, write a failing test for this bug, add labelled logs at each step, and report only what you observed.”

Do it for real

Reading gets you the words. This gets you the experience behind them, and something to show.

Read one path with the agent off

This is the question most likely to come up about your own project: pick a file and walk me through it. It is far better to find the gaps now.

  1. Pick one thing your project does, such as a button press or a request. Close the agent.
  2. Find where it starts using search. Follow the calls by hand and write down each function, with its file and line, and what goes in and comes out.
  3. Run it under a debugger, or with labelled logs, and check your notes against what really happens.
  4. Mark everything you got wrong, and find out why. Only now ask the agent about the parts you still can't explain.

Done when: A page of notes listing the path through your code, with your wrong guesses marked and corrected.

Find a seeded bug without the agent

Reading about halving is not the same as having done it under a little pressure. Afterwards you can say truthfully that you debug without an agent, and show how.

  1. On a new branch of a small project, ask an agent to introduce one subtle bug somewhere, commit it, and not tell you where. Have it write the location in a file you do not open.
  2. Close the agent. Find a way to make the bug happen every time, and write that down.
  3. Shrink the reproduction, then halve your way to the fault using logs, a debugger or git bisect. Write down each hypothesis and what ruled it out.
  4. Write a failing test, fix the root cause, and watch the test go green. Then open the file and check your answer.

Done when: A commit with a test that fails before the fix and passes after it, and a message saying what the root cause was and how you found it.

Take over from a stuck agent

Sooner or later this happens on real work. A write-up of one real rescue is about the strongest evidence of this skill you can show anyone.

  1. The next time an agent fails the same bug about three times, stop it. Don't ask it to try again.
  2. Go back to the last good commit, then write down what you actually know, and how you know it.
  3. Reproduce the bug yourself and find the cause, or get far enough to say something specific and observed.
  4. Give the agent that finding, and note how much better its next attempt is.

Done when: A pull request description or note containing the reproduction, the hypotheses you ruled out, the root cause, and the attempts that had failed before.

The build, stage 4 of 18

A bug found by hand

A bug the agent hid, found by you with the agent closed, and your first hand-written test, which failed before your fix. Your first ratchet row.

About 60 to 120 minutes, on the same app as every other stage. We ran it first and show what went wrong.

Open stage 4 of the build

Gate 4: Reading and debugging

0 of 6 right

  1. 1The cart shows "£NaN". The agent says it is fixed, and the diff is one line: "£" + (total || 0). The test it added checks that the page no longer contains NaN, and it passes. What do you do?

  2. 2A crash trace has six frames. The top two are inside a date library in node_modules. The third is formatOrder in your own src/orders.ts. Where do you start?

  3. 3A nightly job applies 64,000 transactions and the final total is a penny out, with no error at all. You can run it from the start up to any transaction and see the total. What is the quickest way to find the bad one?

  4. 4A failing test passes as soon as the agent adds one log line. It says: "Fixed, all tests green." What have you learned?

  5. 5An agent writes const saved = { ...cart } and then calls saved.items.push("milk"). You check, and cart.items has grown as well. Why?

  6. 6Your agent writes save(order), with no await, followed by showMessage("Saved"). Everything looks right in testing. What can go wrong for real?

Get every answer right to install it, and to put a file in your repo. Wrong answers cost nothing.

Next chapterThe model

The workshop

Name a word, or describe what you mean in everyday language.