Chapter 2. The run-and-verify loop

Learning objectives

  • Explain the relationship between local execution, a development server and ports, and prepare an unfamiliar repository to run.
  • Distinguish the problems caught and missed by tests, type checks, linting and builds.
  • Read what, where and why something failed from the output, and choose the first error to investigate.
  • Narrow the cause of a failure by checking one hypothesis at a time.
  • Verify claims of completion through browser checks and evidence, and disclose anything you could not verify.

Introduction

"Fixed. All tests pass." When working with coding agents, you may see this sentence several times a day. Yet the sentence alone cannot tell you whether tests actually ran, whether they cover this change or whether the problem has disappeared on screen.

One side-project operator trusted such a report and deployed, only to receive a customer inquiry the next morning. All the tests really had passed. They covered calculation functions, however, while the bug was in the screen. Opening the browser once would have revealed it.

This chapter teaches how to turn "It is done" into evidence that it is done. It starts with where an app runs, then covers local setup, the role of each checking tool, reading errors and narrowing causes. It continues with mechanisms that keep the verification loop running without relying on memory, browser checks, evidence requirements and context management.

Key concepts

Where the app runs: browser, server and storage

A web app usually operates in three places. The user's browser draws the screen and receives clicks. The server receives browser requests, performs calculations and returns responses. Storage, such as a database or files, keeps data that must survive a refresh. In a restaurant, the browser is the dining room, the server the kitchen and the database the storeroom.

This distinction matters because errors appear in different places.

Where the problem occurs Where to see the error
Browser (screen code) Browser developer tools console
Between browser and server (requests and responses) Browser developer tools Network tab and server output
Server code Terminal running the development server; server logs after deployment
Database connection Connection errors in server output, such as ECONNREFUSED (connection refused), when the database is off or its address or port is wrong

"The screen said it was saved" and "The data was saved" are two different facts. To confirm that the data was actually saved, refresh the page or open it again on another device.

Running locally: development servers and ports

Running locally means trying the app on your own computer before putting it on the internet. It is like inviting your family to a tasting before the restaurant officially opens. The program that serves the app is called a development server. It opens a port, a numbered door, and waits for requests. http://localhost:3000 means "door number 3000 on my own computer."

If you encounter an EADDRINUSE (address already in use) error while starting a development server, another process is already using that port. Often, a development server you started earlier is still running in another terminal. The response is to stop that process or start the server on another port. Before stopping it, identify the process ID (PID) of the process using the port (lsof -i :3000 on macOS and Linux), then use kill <PID> to terminate only that process. Commands that stop multiple processes at once by a name pattern are dangerous because they can also stop unrelated programs. This error is usually a matter of a port being occupied, so reinstalling does not resolve it. In rare cases, code or configuration that starts the same server twice can also be the cause.

Preparing an unfamiliar repository to run

When you first receive a repository made by someone else or created by an agent, it is tempting to type npm run dev straight away. However, the app can run only after certain preparations are in place. Checking them in order reduces the time you spend trying to find your way.

  1. Read the README and instruction files. These are the places where the commands for running the app and the necessary preparations are documented.
  2. Match the runtime version. If the repository has a version file such as .nvmrc, use that Node.js version.
  3. Install according to the lock file. If there is a package-lock.json, use npm ci to install exactly the versions recorded in it. npm's documentation explains that this command will not run without a lock file. If its contents differ from package.json, the command stops with an error instead of updating them to match, and it modifies neither package.json nor the lock file.
  4. Set environment variables. Read .env.example for required names and put test values in a local file such as .env.local.
  5. Prepare the database. Run any migration command to create the tables.
  6. Start the development server and open one core screen yourself.

The errors that appear when you skip these steps are usually predictable. An error saying that an environment variable is missing signals that you should revisit step 4; Cannot find module points you back to step 3, and a refused database connection points you back to step 5.

A map of checking tools

Automated code checks come in several kinds, each catching different problems.

Tool Analogy What it catches What it misses
Tests A tasting panel Whether specified inputs produce expected results Behavior without tests
Type checks Comparing dimensions on a blueprint Incorrect argument types or argument counts, nonexistent properties Incorrect calculation logic with correctly typed values
Linting A kitchen hygiene checklist Unused variables, risky code patterns, rule violations Whether features behave as required
Build Final assembly before opening Errors that appear only when packaging for deployment Runtime logic errors

In TypeScript, a command such as tsc --noEmit checks types without generating executable files. The TypeScript documentation describes noEmit as using TypeScript as a type checker without emitting compilation output. Add a required function argument and one type check can find incompatible callers. Calls that bypass checks with any can still be missed.

The key is that "pass" means something different for each tool. Passing tests does not prove untested screens correct, and passing type checks does not prove discount calculations correct. Choose checks appropriate to the change and start with lightweight ones.

Anatomy of an error message

Error messages are not long because they are intended to frighten you. They generally provide three kinds of information.

  1. What: the error type (TypeError, a failing test name).
  2. Where: file name and line number.
  3. Why: expected versus actual values, or what was missing.
TypeError: Cannot read properties of undefined (reading 'filter')
    at ClassSchedule (src/components/ClassSchedule.tsx:31:18)
    at renderWithHooks (node_modules/react-dom/cjs/react-dom.development.js:15486:18)

The first line means that the code tried to use filter on something that did not yet have a value (undefined). The stack trace underneath lists the functions the program passed through before the error, starting with the innermost one. A line inside node_modules belongs to a library someone else wrote; a line inside src/ belongs to your code. In most cases, start by opening the topmost line that belongs to your own code: here, line 31 of ClassSchedule.tsx. It is likely that the code tried to draw the list before the data had loaded.

When dozens of errors appear at once, examine the very first error first. If one module cannot be found, cascading errors can occur everywhere that module is used. Fixing the first cause makes most of the errors that follow disappear along with it.

The most reliable indication of whether a command succeeded or failed is its exit code, rather than the text it prints. An exit code of 0 means success, and any value other than 0 means failure. Even if many warnings are printed, an exit code of 0 means the command succeeded; even if the last line is quiet, a nonzero exit code means it failed. In the terminal, use echo $? to see the exit code of the command you just ran. Programs also usually write their results to standard output (stdout) and their errors separately to standard error (stderr). When the output is long and you want to give all of it to an agent, use a command such as npm test > test.log 2>&1. Sending standard output to a file (>) and combining standard error into the same destination (2>&1) ensures nothing is left out.

Narrowing causes from failing output

Finding the cause is like detective work. You gather clues, check suspects one by one and do not arrest someone on the basis of an unverified guess.

  1. Reproduce. Confirm the same steps cause the same failure. Without reproduction, you cannot tell whether a fix worked.
  2. Identify the failing stage. Installation, startup, build and runtime failures require different investigations.
  3. Choose the first relevant error. Set cascading errors aside initially.
  4. Form one hypothesis. Write something testable, such as "This import path changed."
  5. Check only that hypothesis. If you change several places at once, you cannot tell which change had an effect.
  6. Rerun the same check after fixing. Use the exact command that failed originally.

When handing a failure to an agent, attach the complete output and say "Fix the cause; do not suppress the error." Claude Code's best practices similarly recommend providing the original build error, asking for verified success and fixing the root cause rather than suppressing it. Common suppression methods include disabling type checks with comments, skipping tests and changing expected values to match actual results. If the expected value matches the requirement, the code is wrong.

Verification loops and enforcement

A verification loop repeats editing, running checks and reading results until the checks pass. Give an agent the check commands and it can run this loop itself, but people must remember to ask, and agents sometimes forget. Mechanisms can make parts of the loop automatic.

Mechanism What it does Example
Task instruction Specifies checks for this task "Run npm test after implementing and show the output"
Instruction file Gives checks common to every task "Run npm run check when finished"
Hook Runs a check at a defined point without relying on discretion Formatter after each file edit; a check script before finishing
Required CI check Prevents merging changes that fail checks Tests on every pull request; failure blocks the merge button

Claude Code's documentation explains that hooks, unlike instruction files, execute deterministically, ensuring the action occurs. For example, a PostToolUse hook matching Edit|Write runs a formatter after each edit with those tools. It misses files changed through shell commands, so run another check at completion for full coverage. Commit the hook in the project's .claude/settings.json to share it across the team. Interactive sessions require approval to trust that folder before hooks run, and settings can disable hooks. A Stop hook runs a check script when the agent tries to finish responding and makes it continue if the check fails. However, the documentation says that after eight consecutive blocks without a tool call, it allows the next stop, so passing is not unconditionally guaranteed.

On a GitHub protected branch, enabling required status checks before merging means the specified checks must finish successfully, or with skipped or neutral status, before merging into that branch. A reviewer forgetting a check cannot then admit broken changes. Administrators are exempt by default; enable the option prohibiting bypasses for universal enforcement. In CI—continuous integration, which automatically checks each change—install dependencies with npm ci. npm documents it as intended for automated environments such as CI.

Check in the browser like a user

Work that changes a screen is finished only after a browser check. Anthropic's experiments with long-running agents found they tended to mark features complete without proper testing. Explicitly asking them to test through browser automation like a human user uncovered bugs missed by reading code alone. Claude Code's best practices also recommend capturing changed screens, comparing them with the design and fixing differences.

Check the following yourself.

  • Complete the flow of clicking and entering data, then refresh to see whether the result persists.
  • Reduce to a phone width, such as 360px, and check for clipping or overlap.
  • Check the states defined in Chapter 1, such as empty data, invalid input and slow responses.
  • In the developer tools console, check for red JavaScript errors.
  • In the developer tools network tab, check whether requests were actually sent and which status codes came back.

Status codes summarize responses numerically. The 200s indicate success, 400s request-side problems (invalid input, no permission, nonexistent URL) and 500s server-side problems. If a button does nothing and the network tab shows no request, investigate client-side code. If a request fails with 500, inspect server output.

Distinguish claims from evidence

An agent saying "I have finished" is making a claim. Evidence consists of the commands it ran and their original output, screenshots, and the procedures and results of its checks. Claude Code's best practices recommend asking it to show evidence such as test output, the commands it executed and their results, and screenshots of the resulting screen, rather than simply asserting success. Reviewing evidence is faster than running verification again, and the approach also works for sessions you did not watch.

A good completion report states both what was checked and what could not be checked.

Checks run: npm test (42 passed, 0 failed), npx tsc --noEmit (no errors)
Screen checks: phone and desktop screenshots attached. Confirmed saved data survives refresh
Not verified: real text message delivery (tests checked dispatch records only)

An agent working in the same context may repeat the same misunderstanding while "verifying." Claude Code's documentation therefore describes having a subagent with fresh context challenge the result, so the implementing agent does not grade its own work.

Start afresh when context gets tangled

As Chapter 1 explained, failed attempts accumulate in conversation context too. Repeated fixes can leave a history of failed approaches that encourages the same mistakes. Claude Code's best practices recommend using /clear and restarting with more specific instructions incorporating what you learned if more than two corrections in one session still fail. A clean session with better instructions is almost always preferable to a long session full of corrections.

Remember two things before restarting. First, clearing the conversation leaves the code on disk unchanged. If failed attempts left edits, decide separately whether to return to the last verified commit (Chapter 3). Second, leave a handoff note for the new session: what you tried, what failed and which hypotheses were ruled out. This prevents repeating the same investigation.

If you want to retain the conversation, /compact <instructions> summarizes it with the specified focus.

Going deeper

Passing tests and useful tests

A passing test is not evidence of a useful test. Useless tests pass even when a bug exists. Four common examples follow.

Useless test What it looks like How to check
Copies the implementation Calculates expected values the same way as the code Check whether expectations come independently from requirements
Weak assertion Checks only that "a result exists" Look for comparisons of specific values or states
Has never failed Passed even before the fix Temporarily undo the fix and check for failure
Adjusted to match the result Changes expectations after a failure Look for expected-value edits in the test diff

The strongest check is to see it fail first. Write a reproduction test before fixing a bug and confirm it fails before the change. Claude Code's best practices give an example of describing symptoms and asking the agent to "Write a failing reproduction test, then fix it." Only a test that fails before and passes after demonstrates that fix.

A similar trap is bulk-updating snapshot tests, which store earlier screen or output states for comparison. Overwriting all failed snapshots as the "new baseline" accepts both intended and unintended changes. Check each failed snapshot against the task's intended changes before updating it.

Tests that sometimes fail

A test that sometimes passes and sometimes fails even though the code has not changed is called a flaky test. You may want to move on if you run it again and it passes, but in most cases there is a real underlying cause.

  1. Tests share state with one another. If they use the same test database or global variables, data left behind by an earlier test can break a later test. A typical sign is a test that passes on its own but fails when you run the whole suite. Playwright's documentation stresses that each test must be completely isolated and run independently to improve reproducibility and prevent failures from cascading.
  2. The waiting strategy is wrong. If a screen element takes time to appear and you check only once whether it is there, the outcome depends on that particular moment. Playwright's web-first assertions (await expect(locator).toBeVisible()) wait and retry until the condition is met, whereas checking directly with isVisible() returns a result immediately without waiting. Pausing for a fixed amount of time still fails in a slow environment and wastes time in a fast one.
  3. The test depends on something external. The result changes according to a real external API or the current time. In tests, make the responses and the time fixed.

Configuring retries only hides the symptoms; it does not remove their cause. To fix tests that occasionally fail in CI, you need information from the time of the failure. Playwright's documentation therefore advises using the trace viewer—a tool that lets you retrace a recorded execution step by step—instead of video or screenshots when analyzing CI failures. It also advises configuring traces to be recorded on the first retry of a failed test.

Different results on your computer and elsewhere

"It works on my machine" is a longstanding development joke. Different results mean an environmental difference caused them, usually one of these.

Difference Example How to align
Runtime version Different Node.js major versions locally and in CI Add a version file and use the same version in both
Installed dependencies npm install installed different dependency versions Commit the lock file and use npm ci in CI
Environment variables Values exist only in your local file Document required names and configure CI and deployment too
Operating system macOS APFS is case-insensitive unless a case-sensitive format was selected; Linux is case-sensitive Match import path capitalization exactly to real file names
Execution mode Only the development server was checked, without a production build Run the build locally before deployment

Environment variables are particularly tricky. Next.js documentation says .env*.local files should be excluded through .gitignore, and the test environment (NODE_ENV=test) does not load .env.local. This is designed to make tests consistent across machines. If a test depends on your .env.local, failure elsewhere is expected.

Performance has the same issue. Development servers run code differently from production builds. Compare before and after under the same conditions, close to deployment conditions.

At work

Salt Bread Bakery: 9 a.m. will not save

A customer reported that the booking app for the neighborhood shop "Salt Bread Bakery" would not save a selected booking time. Before trying to fix it, operator Minjae reproduced the symptom. Choosing an afternoon time saved successfully, but choosing a time in the 9 a.m. slot and pressing Save produced no response. The developer tools console was quiet, while the network tab contained a record of a request being sent and returning 400. The problem was with the contents of the request rather than the screen.

Minjae gave the reproduction steps to the agent and said, "Add a test that catches this symptom first, and show me that it fails before you fix it." The agent suggested changing the format-checking code immediately, but Minjae rejected that suggestion. Without first seeing the test fail, he could not trust a passing result after the fix. As expected, the test failed, and the output showed Expected: "09:00" / Received: "9:00". The time string was missing its leading zero and was being rejected by the server's format validation.

After the agent fixed the time-building function, the new test passed. It reported "Done," but Minjae continued. He ran all tests and type checks, selected 9 a.m. in the browser, saved and confirmed a 200 response in the network tab. The booking still existed after refreshing. Only then did he commit.

It was because the symptom had been reproduced that a test could be written; because the test had been seen failing before the fix that it could be trusted; and because the result had been checked in the browser that Minjae could say the symptom was gone.

Green Star Botanical Garden: facing 47 errors

Jiho, a developer running the members' plant-care app for "Green Star Botanical Garden," asked an agent to organize scattered plant helper functions into src/lib/plants/. Running npm run build afterward produced 47 errors.

The agent offered two options: add comments skipping type checks on every error line, or revert the entire reorganization. Jiho rejected both. The first hid errors while preserving their causes; the second discarded the requested work too.

Jiho read the output from the beginning. The very first error was Cannot find module '@/lib/plant-utils' in src/app/watering/page.tsx. Most of the errors that followed appeared to be cascading errors in each file that used functions and types it could no longer import from that module. Jiho formed a single hypothesis: "Some files still import the old path." He then asked the agent to investigate only that hypothesis.

Find all files importing the old path '@/lib/plant-utils' and update them to the new path.
Do not change other code. Then rerun npm run build and show the complete output.

When the build was run again, the error count fell to two. Those remaining two errors had a different cause. During the reorganization, the watering calculation function had been changed to take an additional season value, but two places calling it still used the old form. Jiho considered whether this change had been part of the request. Since he had asked only for folder reorganization, changing the function's interface was outside the scope. Once he had the agent restore the function to its original form, the build passed.

Jiho then ran type checks and tests, opened the watering schedule in the browser and confirmed unchanged dates. Forty-seven errors had two causes. Starting with the first error and checking one hypothesis at a time let him fix them without concealing errors or discarding the work.

Common mistakes

  1. "Tests passed, so we are done." A passing result looks like the end of the work. However, tests check only the scope they cover. If you changed a screen and there are no tests for that screen, the work is not finished before a browser check.
  2. "Fix a failing test by changing its expected value." Changing just one number is quick. However, if that expected value matches the requirement, the code is the part that is wrong. Be most careful when an agent suggests changing a test or skipping it.
  3. "It passed on rerun, so it is fine." A passing rerun reassures, but intermittent failures usually have causes such as shared state or incorrect waiting. Consistent failure on the same value indicates logic errors. Reruns cannot replace analysis.
  4. "Many errors mean many problems." Large numbers feel overwhelming, but dozens of errors often cascade from one cause. Start with the first.
  5. "The agent said it was certain." A confident report inspires trust. However, confidence is not evidence. Request evidence such as the commands, their output and the screens, and ask what the agent was unable to verify.
  6. "Clearing the conversation resets the work." A new session feels like a fresh start, but disk changes survive cleared context. Decide separately through Git whether to undo them.

Summary at a glance

Topic Remember
Error location Screen: console; requests: network tab; server: terminal and logs
Port collision EADDRINUSE means an occupied port. Identify the PID and stop only that process
First run README → runtime version → npm ci → environment variables → migrations → run
Checking tools Tests check behavior, types check forms, lint checks patterns, builds assemble for deployment
Reading errors What, where, why. Topmost own-code line, first error, exit code
Narrowing causes Reproduce → identify stage → first error → one hypothesis → check → rerun same check
Enforcement Increasing strength: instruction < instruction file < hook < required CI check
Browser Full flow, narrow screen, persistence after refresh, console and status codes
Evidence Commands, original output, screenshots and what was not verified
Context After more than two failed corrections, /clear and better instructions. Undo code separately

Check the official docs

Self-check

2-1. When you start the development server, it produces Error: listen EADDRINUSE: address already in use :::5173. What does this mean, and how should you respond? Also give one example of a method you should avoid when responding.

2-2. An agent reports, "The test run produced 0 failures." When you open the output, it reads Tests: 0 passed, 0 total. Why must you not accept this result as a passing test run, and what cause should you suspect first?

2-3. Pressing the Send button on an inquiry form produces no response. What should you look for in the console and the network tab of the browser developer tools respectively, and how do those results tell you where the problem lies?

2-4. Which file and line should you open first in this output, and why?

TypeError: Cannot read properties of null (reading 'price')
    at sumLines (src/checkout/summary.ts:47:12)
    at OrderPanel (src/components/OrderPanel.tsx:18:9)
    at renderWithHooks (node_modules/react-dom/cjs/react-dom.development.js:15486:18)

2-5. A build prints dozens of yellow warnings and no error message on its final line. What is the most reliable way to determine whether it succeeded?

2-6. An agent adds a new test while fixing a discount calculation bug, and it passes. Describe a simple way to confirm that the test really catches the bug.

2-7. An app works on a MacBook but fails only in Linux CI with Module not found: Can't resolve './components/header'. The actual file is Header.tsx. What causes this and how do you fix it?

Hands-on tasks

Exercise

One loop using failing output (45 minutes)

  1. Deliberately introduce a bug into a practice app function, such as omitting the final item from a total.
  2. First write a test catching that bug and personally confirm that it fails.
  3. Write the what, where and why from the failing output on paper.
  4. Give the agent the full output and ask, "Fix the cause without changing tests. Show full test output and the exit code."
  5. Temporarily undo the fix, confirm the test fails again, then restore the fix.

Done when: your recorded cause matches the location the agent fixed, before-fix failure and after-fix passing output (exit code 0) are both saved, and the test file is unchanged.

Exercise

Make verification automatic (50 minutes)

  1. Write one line in the instruction file giving the check command to run at the end of a task, such as npm run check.
  2. Use your tool's hooks to run a formatter or lint after each file edit, and commit the configuration file (PostToolUse in .claude/settings.json for Claude Code).
  3. Delegate a small screen change and look for evidence that the hook actually ran.
  4. View the result at phone and desktop widths in the browser. Record console errors, if any, and network response status codes.
  5. Organize the completion report into three fields: checks run, screen checks, not verified.

Done when: there is a committed hook configuration, a record shows that the hook ran even though the agent was not separately instructed to run it, and the three-field completion report either has a filled-in "Not verified" field or explains why that field is empty.