Without notes, state yesterday’s main idea and one unresolved question.
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:
- Who — which user and role you were signed in as.
- Exact steps — clicks and inputs, in order, with the real values you typed.
- Expected — what should have happened.
- Actual — what did happen, quoting error text exactly rather than paraphrasing.
- 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. Thefieldsobject 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:
- UI — Network shows one
GET /api/equipment. A request was sent, so the handler and effect are fine. - Network — the API log has the matching line. It arrived.
- API — status
200. Not auth, not validation, not a crash. - Rule — no error logged, no exception. The service did what it was told.
- Contract — open the Response tab and read the actual JSON:
{"id":12,"name":"Pump 3","state":"active"}. The frontend readsitem.status, which isundefined, and React renders nothing forundefined. 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
- Pick one break from the
[!try]list — or invent your own — and apply it. Commit nothing yet. - Write the reproduction: who, steps, expected, actual, frequency. Confirm it fails 3 of 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.
- Use
curlat least once to half-split, and record whether it cleared the frontend or the backend. - Find the root cause and state it in one sentence that names a file and a line.
- Fix it. Change one thing. Re-run the reproduction and confirm 3 of 3 now pass.
- Write
docs/incident-01.mdwith these headings: Symptom, Reproduction, Evidence, Root cause, Fix, Prevention. - 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.
- This lesson used
node --inspect. Find what--inspect-brkdoes differently and one situation where only that flag helps. - The guide names a security consideration about the inspector port. Find it and say what it means
for running
--inspecton 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
- 0–5 min Recall
- 5–20 min Learn
Read only the listed concept notes and official reference sections needed today.
- 20–48 min Build
Break one request intentionally, then identify whether failure is UI, network, API, rule, or database.
- 48–55 min Explain and verify
Run the result, inspect evidence, and explain the data/control flow in your own words.
- 55–60 min Quiz and commit
Complete the quiz, record one lesson, and commit the verified change when applicable.
What to hand in
A written incident report with symptom, evidence, root cause, fix, and prevention.
Working with AI today
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
Explain-back gate
Pass the quiz above to unlock completion.
Quiz + explain-back checks required.