Reading the Error Message
An error message answers four questions if you let it: what failed, where, with what input, and which assumption broke. Searching the exact string first skips all four and hands your hypothesis to whoever wrote the top result.
The situation, the reflex, and why it stalls
Every lesson starts where being stuck starts: someone has a problem, and the first move that comes to mind feels like progress.
A stack trace is on the screen. What do you read before you search for it, and what does each part tell you?
The backend log for the failing checkout shows TypeError: Cannot read properties of undefined (reading 'id') with a stack trace through the handler, the ORM and the framework. Your cursor is already in the search bar with the message pasted.
Search the exact message. Someone has seen it before; the top result has a fix; the fix is a copy-paste away. It feels like the fastest route and sometimes it is.
The message is generic. Every property read on an undefined value produces it, in every codebase, for every cause; the search returns a thousand fixes for a thousand bugs, none of them yours.
- The message is generic. Every property read on an undefined value produces it, in every codebase, for every cause; the search returns a thousand fixes for a thousand bugs, none of them yours.
- The top result proposes a cause and you adopt it. Now you are testing a stranger's hypothesis about a different system, and it is often the wrong one wearing the right words.
- The parts of the message that are specific — the property name, the file, the line, the frame that belongs to your code — go unread, because the search bar took them as one opaque string.
- When the search fails, there is nothing to fall back on, because the reading that would have produced a hypothesis was skipped.
The move
Precisely enough to apply it to a problem you have never seen — not a slogan.
- Read the message as a structured answer before treating it as a search key. What failed: the operation that could not be completed. Where: the first frame in the trace that is in your code, not the framework's. With what input: the value that was wrong — here, something that was undefined. Which assumption: what the code at that line believed about its input that turned out to be false.
- Then sharpen the assumption into a question — Unknown to Specific Question applied to a stack trace. "cart.items[0].id is undefined" becomes "why does this handler receive a cart whose items have no id, when it assumes they do?" That question has an experiment; the message alone did not.
- Search after, and search for the specific part: the library and the operation, or the assumption, not the generic message. The result is now something to check against your hypothesis, not something to adopt in place of one.
- For an error from a library or a provider, read its documentation for that error code before reading a forum about it; the documentation says what the library meant, and the forum says what someone guessed (Reading Documentation With a Goal).
From the message to a question
The same failure, asked three ways. The vague form is what gets pasted into a search bar; the best form is what gets answered by one log line and a diff. The difference between them is entirely produced by reading the message.
item.product undefined in buildOrderLines for this checkout?”why The best form names the value, the assumption and a candidate cause with a diff to read. It can be answered by logging the cart at the handler entry and reading one commit. The vague form can only be answered by a stranger describing a different bug.
Reading the trace against the four questions
A trace has more structure than a search bar sees. The table walks one, top to bottom, and says what each part answers. Most of the framework frames answer "where was the problem detected"; only the frame in your code answers "where was the assumption made".
| Part of the message | What it answers | In the example |
|---|---|---|
| Error type and text | What failed | A property read on undefined — a missing value, not a wrong one |
| The property or operation named | On which value | id — so the thing missing is whatever .id was read from |
| Framework and library frames | Where the failure was detected | ORM serialisation, router — they received a bad result, they did not make it |
| First frame in your code | Where the assumption lives | checkout/pay.ts:41, buildOrderLines: assumes each item carries a product |
| "Caused by" chain, error codes | The underlying failure when wrapped | Absent here; present for provider and database errors, and usually the most specific line |
| Request id, timestamp | Which request, so the log can be filtered | r-8c1f, which links this trace to the cart logged at handler entry |
Errors that are not bugs
Not every error message reports a defect. Some report the system working as designed — a decline, a validation rejection, a timeout on a provider that is genuinely down. The table separates them, because the next question is different for each: a bug wants a cause, a decline wants a product decision, a timeout wants a policy.
| Trigger | Symptom | Cause | Response |
|---|---|---|---|
| TypeError / null reference in your frame | 500 to the client, "Payment failed" in the UI | An assumption about input shape broke — usually after a change upstream | Find where the input was produced; fix the producer or the assumption, not the line. |
| Provider returns a decline code | 402 with a reason; the customer sees a failure | The provider did its job; nothing in our system is wrong | Decide what the customer sees and what happens to the order (What If Payment Fails?). |
| Validation error from our own API | 400 with a field name | The frontend sent something the contract forbids | Read the contract; the bug is in the client or in the contract, not in the validator (Validation Errors: Feedback, Not Verdicts). |
| Timeout on the provider call | Slow failure; sometimes the charge happened anyway | The provider is slow or down; the outcome is unknown, not failed | Treat as ambiguous, not as failure; the next question is about idempotent retry (A Timeout Tells You Nothing About Whether It Happened). |
| Deadlock or constraint error from the database | Error after the provider succeeded | Two requests contended for the same rows, or an invariant held | The constraint may be correct and the duplicate request the bug (Duplicate Requests). |
How to do it
Most important first.
- Read the whole message, including the parts after the first line: error codes, the property name, the "caused by" chain. Most people stop at the first line.
- Walk the trace from the top until the first frame in your own code. That is where the assumption lives; frames above it are the library discovering the problem, not causing it.
- Name the value: what exactly was undefined, null, missing or malformed? If the message does not say, the first experiment is to print or inspect that value at that frame.
- Write the broken assumption as a sentence, then as a question with an experiment (Question Quality).
- Search with the specific parts — library name, operation, error code — and read results as candidate hypotheses to test, not as fixes to apply (Evaluating What the Search Returned).
Worked on a concrete problem
The move has to produce something. This is what it produced.
- The message:
TypeError: Cannot read properties of undefined (reading 'id'). What failed: reading.id. Where: the trace has six frames in the framework and ORM and thencheckout/pay.ts:41, inbuildOrderLines. With what input: at line 41,item.product.id— soitem.productis undefined for some item. Which assumption: every cart item has a loaded product. Question: why does a cart item arrive without its product? Experiment: log the cart at the top of the handler for the failing request id. The cart refactor last night changed items from a list of objects with products to a map keyed by product id; the handler was never updated. Nobody's search result could have said this. - A provider error:
card_declined: insufficient_fundswith an HTTP 402. What failed: the charge. Where: the provider, not us. With what input: a specific card. Which assumption: none broken — this is the provider doing its job. The question becomes a product one: what should the customer see, and should the order stay open? Searching the message would have led to a page about test card numbers. - The "google the error" slogan, made falsifiable: it helps when the message is specific to a library or an environment — an error code, a version conflict, a misconfiguration — and it fails when the message is generic to the language. The property name and the frame decide which kind you have, and you only know them by reading first.
How you know it worked
What now exists that did not before, and what question you can now ask.
- You can say, for the error on screen, what failed, in which of your frames, on which value, and what assumption broke.
- You have a question with an experiment before you have opened a search tab.
- Search results are being checked against your hypothesis rather than replacing it.
- The fix, when it comes, addresses the assumption, not the symptom — the cart shape, not a null check at line 41.
The questions you can now ask
The field this whole domain exists for. After this lesson, these are the questions to put to an unfamiliar problem.
- ?What operation failed, in which frame of my own code, on which value?
- ?What did the code at that line assume about its input, and where did the input come from?
- ?Is this error mine, the library's, or the provider's doing its job — and does each need a different next question?
- ?What specific part of this message is worth searching, and what would a useful result have to tell me?
What can go wrong
- Reading the trace and stopping at the first frame in your code when the cause is upstream: the frame shows where the assumption broke, and the value was produced somewhere earlier (Follow the Data).
- Papering over the assumption. A null check at the failing line makes the message go away and lets the malformed cart continue to the provider; the assumption was correct and the input was wrong.
- Never searching. For a library error code, the documentation and a good search result are the fastest route to what the library meant; refusing them is delegation avoidance turned into slowness.
- Reading a message from the wrong layer. The frontend's "Payment failed" is not the error; the backend's 500 is not the error either; the error is the exception that produced the 500, and it may be three layers down (Zoom In, Zoom Out).
- Reading takes longer than pasting, and for a well-known environment error — a missing dependency, a port in use — the paste wins and the reading is redundant.
- Stack traces in some runtimes and frameworks are deep and mangled; finding the first frame of your own code can take effort, and source maps or symbolication may be needed before the reading is possible.
- The assumption-as-question step produces an experiment, which is more work than a guessed fix and is the reason the fix holds.
- "Never search error messages." Search after reading, and search for the specific part. The domain is pro-tool; the objection is to letting the search bar do the reading.
- "The line in the trace is the bug." It is where the assumption was detected. The bug is wherever the input was produced, which can be a different module, a different service, or a data migration.
- "Framework frames are noise." They are context: the frame where a library validated your input and rejected it says what the library expected, which is often the assumption you broke.
Where this applies
Problem-solving advice is stated as universal far more often than it is. These labels say what each method is specific to — and where CONTESTED appears, the note gives the strongest form of the opposing view.
- GENERALStack traces, compiler errors, HTTP error bodies, provider error codes and database errors all answer the same four questions; the fields are in different places.
- TEAM-SPECIFICFor a learner, reading first is the whole point — the manifesto's "debug without AI" starts here. For a senior on a deadline, the reading is a two-second glance and the search follows immediately; the order is the same and the visible time differs.
- ILLUSTRATIVEThe TypeError, the frame at line 41 and the cart-as-map refactor are invented to show the reading producing a question; no real trace is quoted.
Where the depth lives
This domain asks the question and hands the answer off by name.
- — A Programming Languages & Runtime Internals domain does not exist yet; how a runtime builds a stack trace, what a source map restores, and why some frames are missing belong there.