"The save button doesn't work."

That sentence contains no information. Claude Code will still answer it, confidently, because it always answers: it guesses at a cause, changes a file, and hands you back something that compiles. When the guess is wrong you now have two problems, and the second one is yours.

The idea

Software fails in layers, and each layer keeps its own record. The browser knows the request came back with an error. The server knows which line of your code threw. The database knows the rule that was violated. Only the deepest layer holds the cause, and only the top one is what your user sees. The whole skill is walking down that stack, copying the exact words at each level, until you reach a message that explains rather than reports.

code
  you click Save
        |
        v
  [ browser ]    devtools > Console:  POST /api/orders 500
        |                              (a symptom, not a cause)
        v
  [ server  ]    journalctl -u myapp:  TypeError at orders.ts:41
        |                              (where it stopped)
        v
  [ database ]   null value in column "email" violates not-null constraint
                                       (the cause)

How it works

Three places hold the record, and you should know how to open all three.

  • The browser console. Ctrl+Shift+J in Chrome and Ctrl+Shift+K in Firefox go straight to it on Windows and Linux; on a Mac that is Option+Command+J and Option+Command+K. In Safari, switch developer features on first (Settings, Advanced, "Show features for web developers"), then Option+Command+C. F12 also opens devtools, but on whichever panel you used last, so you may land on the HTML tree and have to click Console. Red lines are errors your page hit while running, and the Network tab beside it shows every request and what came back. A red 500 there means your server failed, so stop looking at the frontend.
  • The terminal. While npm run dev is running, the terminal window it is running in is your server log. It scrolls past fast. Scroll back.
  • The server logs in production. If you pushed to Vercel, Railway or Fly.io, it is a Logs tab in that dashboard, which keeps two streams: a build log for the deploy and a runtime log for requests after it. If you set up a Linux server yourself, it is journalctl. Find yours while nothing is broken.

myapp in these commands is a placeholder. Everywhere it appears, substitute the name your own service runs under, which the first command finds. Getting it wrong is quiet rather than loud:

code
systemctl list-unit-files --type=service | grep -i myapp
sudo journalctl -u myapp -n 200

Use list-unit-files rather than list-units: the second hides a service that is simply stopped, so you conclude you guessed the name wrong.

-- No entries -- does not mean there were no errors. It usually means the unit name is wrong, or that you cannot see the system journal as this user, which is why the sudo is there. Treat an empty result as a question about your command.

A reads top down. The first line is the error type and message; the lines under it are the trail of calls that got there, most recent first. Skip everything inside node_modules. The first line naming a file you wrote is where you start, because that is the first place you have any control.

In production that rule needs one adjustment. The built JavaScript is minified, so a browser trace points at .next/static/chunks/... with no filename you recognise, and Next.js replaces a server-side error in the browser with a generic message and a digest value. That digest joins the two halves: search your server log for the same string and the readable trace is there. When a browser trace has nothing recognisable in it, that is expected, and the server log is where to go.

What to do

  1. Copy the exact message. Not your paraphrase of it. The literal string, including the file name and line number, because the search value is in the exact wording.
  2. Write down the smallest sequence that makes it happen again: what you clicked, what you typed, what you expected, what appeared. Without that, you cannot tell whether a fix worked.
  3. Strip the log before it goes anywhere. Take the twenty lines around the failure rather than the two hundred, then read them for email addresses, tokens, cookies and anything shaped like postgres://. Replace those with REDACTED rather than deleting the line, so the structure stays intact.
  4. Give the AI all of it at once: the trace, the reproduction steps, and the relevant file. Then ask what the trace implies before asking for a fix. "Which line failed, and what was null?" gets a better session than "fix this".

Where it breaks

Logs go missing. Many hosts keep only a rolling window (Vercel's Hobby plan keeps runtime logs for an hour), so a failure from last Tuesday may be gone before you look. Find out today what your retention is, because it decides whether a problem you notice late can be investigated at all.

Some failures also refuse to reproduce. Anything depending on timing, on two requests arriving at once, or on one specific user's data will look fixed the moment you retry it. Cannot reproduce is not the same as fixed. Leave it open.