0 / 91
Week 10 · Day 69 of 91

End-to-end debugging

Connecting the Full Stack

Objective

Diagnose failures by narrowing the layer instead of prompting blindly.

This is system integration: individual modules may pass bench tests but the complete signal path must also be verified end to end.

  • reproduction steps
  • network evidence
  • server logs and SQL inspection

Why this matters

Everything you built this week has at least five places to go wrong, and from the user's chair they all look the same: the screen is not right. The difference between a developer who fixes that in ten minutes and one who loses a day is not knowledge of React or SQL. It is a method — a way of eliminating layers with evidence instead of changing things and hoping.

Today you break your own application on purpose, find the fault by narrowing, and write it up. That write-up is a real professional artifact; it is also the thing that stops the same bug happening twice.

Reproduce it first

Before any theory, get the failure to happen on demand. A bug you cannot reproduce is a bug you cannot prove you have fixed — you will change something, not see the symptom, and ship a guess.

A usable reproduction has five parts:

  1. Who — which user and role you were signed in as.
  2. Exact steps — clicks and inputs, in order, with the real values you typed.
  3. Expected — what should have happened.
  4. Actual — what did happen, quoting error text exactly rather than paraphrasing.
  5. Frequency — 3 of 3 attempts, or 1 of 10. Intermittent is a different investigation.

Then make it minimal. Remove steps until it stops failing; the last step you removed is part of the trigger. "Log in, browse, search, click machine 7, submit the form" often reduces to "submit the form with an empty notes field", and that shorter sentence has already told you where to look.

Signal tracing, not shotgunning

Faced with a dead output, a bench technician does not start replacing components. They probe at the midpoint of the chain: if the signal is good there, the fault is downstream; if not, upstream. One measurement halves the suspect region. Reflowing joints at random is what people do instead of measuring, and it is the same instinct as retrying the request and restarting the server.

Narrowing the layer

Your request passes through five layers. Each one has an observation that either clears it or convicts it. Work through them in order and the answer falls out.

# Layer The question Where you look
1 UI Did a request leave the browser at all? DevTools → Network
2 Network Did it arrive at the server? your API's access log
3 API What status did it answer with? Network row + server log
4 Rule Did the business logic do what you meant? structured logs (Day 54)
5 Database What did SQL actually return? logged query, or run it in psql

No row in the Network panel means nothing left the browser: a handler not wired up, a client validation returning early, an effect that never ran. The whole server is innocent and you can stop reading its logs.

A row in the browser but no line in the server log means it never arrived: wrong base URL, wrong port, server not running, or a CORS preflight refused. Note that a CORS error is the opposite case — the request arrived and was answered, and the browser refused to hand you the result. That is why "is there a line in the server log?" is the question that separates these two, and why guessing is so unreliable here.

A status code is the fastest single clue you get:

  • 400 — your request body was rejected by validation. The fields object says which field.
  • 401 — not authenticated. The session cookie was missing or expired.
  • 403 — authenticated but not permitted. Your Day 52 rules are working.
  • 404 — the path is wrong, or the row genuinely does not exist. Check which by trying a known id.
  • 409 — a constraint conflict, usually a duplicate.
  • 500 — the server threw. This one is always your bug, and there is always a stack trace on the server. Go and read it.

Finding the leak

A plumber does not open every wall. They check the meter, then the stopcock, then under the sink — each check rules out a whole section of pipe. Same water, same house, one section at a time.

Half-split with curl

The most valuable single move is not first in the list. Send the request without the browser:

curl -i -b "session=<your cookie>" \
  'http://localhost:3000/api/equipment?status=active&page=1'

If curl gets the right answer, the entire backend is cleared and the bug is in the frontend or in how it built the request. If curl gets the wrong answer, the entire frontend is cleared. One command, half the system eliminated. In DevTools you can right-click any Network row and choose Copy → Copy as cURL to reproduce exactly what the browser sent, headers and all.

Looking inside the server

When the fault is behind the API, console.log is legitimate and often enough — log the input at the top of the service, the value at the decision point, and the SQL parameters before the query. Structured logs with a request id (Day 54) let you follow one request through all of it.

When printing is not enough, Node runs a debugger:

node --inspect server.js
Debugger listening on ws://127.0.0.1:9229/8f2c1f9e-...
For help, see: https://nodejs.org/en/docs/inspector

Open chrome://inspect in Chrome and click inspect to attach: you get breakpoints, step execution, and the ability to read every variable at a point in time rather than the two you thought to print. Use node --inspect-brk to pause before your code starts, and a debugger; statement in the source to stop at an exact line.

And for the last layer, do not infer what the database did — go and ask it. Open psql and run the same query with the same parameters. If the row is there and the API says it is not, the fault is between them. If the row is not there, no amount of frontend work will help.

Walkthrough: one deliberate break

Rename a field in the API response so it no longer matches the frontend's expectation — change the repository's SELECT to SELECT id, name, status AS state FROM equipment. Restart the API and open the list.

Symptom: every machine's status shows as blank. Nothing errors. This is the nastiest class of bug, because every layer reports success.

Now narrow:

  1. UI — Network shows one GET /api/equipment. A request was sent, so the handler and effect are fine.
  2. Network — the API log has the matching line. It arrived.
  3. API — status 200. Not auth, not validation, not a crash.
  4. Rule — no error logged, no exception. The service did what it was told.
  5. Contract — open the Response tab and read the actual JSON: {"id":12,"name":"Pump 3","state":"active"}. The frontend reads item.status, which is undefined, and React renders nothing for undefined. Found it.

The bug was never in a layer; it was between two of them, in the agreement about field names. As you saw on Day 64, TypeScript did not catch it because response.json() is any — which is exactly why the boundary check you wrote there matters, and why it is worth extending until it would have caught this.

Break it four ways and time yourself

Do each of these, one at a time, and write down which of the five layers each symptom eliminates before you fix it: (a) change VITE_API_URL to port 3001; (b) remove credentials: true from your cors() options; (c) send a status value your CHECK constraint rejects; (d) remove the await before an apiFetch call. Each produces a different, recognisable signature. Learning the signatures is the skill.

Reviewer mode — use it as an interrogator, not an oracle

"Do not guess. Ask me for evidence from each boundary and help eliminate layers systematically." Paste the reproduction steps first and refuse to paste code until it asks for a specific observation. If it proposes a fix before you have supplied evidence, tell it so and repeat the instruction. The output you want is specific, evidenced findings — never a rewrite of files it has not seen fail.

Your turn

  1. Pick one break from the [!try] list — or invent your own — and apply it. Commit nothing yet.
  2. Write the reproduction: who, steps, expected, actual, frequency. Confirm it fails 3 of 3.
  3. Walk the five layers in order, writing down the observation at each and what it eliminates. Do not skip ahead even when you think you know, because you introduced this bug and your instinct is contaminated.
  4. Use curl at least once to half-split, and record whether it cleared the frontend or the backend.
  5. Find the root cause and state it in one sentence that names a file and a line.
  6. Fix it. Change one thing. Re-run the reproduction and confirm 3 of 3 now pass.
  7. Write docs/incident-01.md with these headings: Symptom, Reproduction, Evidence, Root cause, Fix, Prevention.
  8. Under Prevention, name one concrete change — a boundary type check, a clearer error message, a log line that would have made this obvious — and say whether you implemented it.

You are done when

Your incident report lets someone who was not there reproduce the bug, follow your elimination path, and see why the root cause explains every observation — including the ones that looked fine.

Common pitfalls

  • Fixing before reproducing. You change three things, the symptom goes away, and you never learn which one mattered or whether it is really gone.
  • Changing several things at once. Now you have two unknowns instead of one, and possibly a new bug wearing the old one's symptoms.
  • Assuming a CORS error means the server failed. It usually answered fine. The server log settles it in five seconds.
  • Trusting a 200. A successful status only says the server answered. Read the response body before concluding the API is innocent.

Verify it yourself

Open today's reference, the Node.js debugging guide, and find where it explains attaching an inspector.

  1. This lesson used node --inspect. Find what --inspect-brk does differently and one situation where only that flag helps.
  2. The guide names a security consideration about the inspector port. Find it and say what it means for running --inspect on anything that is not your own machine.

Add both to docs/incident-01.md under a Tools heading. Knowing your debugger's limits before you need it is the difference between reaching for it and avoiding it.

The hour

  1. 0–5 min Recall

    Without notes, state yesterday’s main idea and one unresolved question.

  2. 5–20 min Learn

    Read only the listed concept notes and official reference sections needed today.

  3. 20–48 min Build

    Break one request intentionally, then identify whether failure is UI, network, API, rule, or database.

  4. 48–55 min Explain and verify

    Run the result, inspect evidence, and explain the data/control flow in your own words.

  5. 55–60 min Quiz and commit

    Complete the quiz, record one lesson, and commit the verified change when applicable.

What to hand in

Deliverable

A written incident report with symptom, evidence, root cause, fix, and prevention.

Working with AI today

AI as skeptical reviewer

Provide existing work and ask for concrete defects, risks, missing tests, and unsupported assumptions—not praise.

Do not guess. Ask me for evidence from each boundary and help eliminate layers systematically.

References

End-of-day quiz

Q1 What is the first debugging goal?
Q2 Which result best proves today’s work is complete?
Q3 What should an AI code review primarily produce?

Explain-back gate

Pass the quiz above to unlock completion.

Quiz + explain-back checks required.