Why this deep dive exists
Nobody reads your architecture doc under stress. Everybody reads the error message the instant their pipeline dies. A good one turns a 30-minute Slack thread into a 30-second fix. A bad one ("Error: failed") sends a person to read your source code at 2am. This deep dive treats every message your program prints as a tiny, high-traffic document.
Errors are documentation 10 min
Before wording, the engineering has to be right. A perfectly worded message about the wrong problem is worse than useless. Google's course opens with the rules that happen in code, not in copy.
Self-studyDo not fail silently, do not swallow the cause4 min▶
The two deadliest error-handling sins both hide information the reader needs:
- Failing silently is when something goes wrong and the program says nothing. Pulse's clean.py currently does this: it drops rows with bad timestamps or invalid event types and never mentions it. Your metrics are quietly wrong and nobody knows why.
- Swallowing the root cause is when a specific failure gets flattened into a generic "Server error" or "Something went wrong". The real cause - a missing column, a bad path - is right there in code, and you threw it away before the reader could see it.
- Raise immediately. Report the error at the moment and place it happens, not three functions later where the context is gone.
The silent drop that cost a quarter. A revenue pipeline silently discarded rows whose currency field it did not recognize. For three months the dashboard was 4% low and nobody questioned it, because nothing ever complained. The fix was one log line: "dropped 812 rows: unknown currency 'GBP '". The trailing space had been the bug the whole time.
Self-studyThe reader is already frustrated3 min▶
Every other doc you write is read by someone who chose to read it. An error message is read by someone who was stopped mid-task against their will. That changes the writing job:
- Respect the moment. They do not want prose, backstory, or an apology. They want to know what broke and what to do.
- Brevity is kindness here. Every extra word is read through gritted teeth.
- The message is a promise. If it says "add X to config.yaml", that had better be the actual fix - a wrong instruction here destroys trust faster than anywhere else in your docs.
The 13 rules, in three groups 20 min
Google's course is one rule per lesson. They cluster into three jobs a message must do: say what happened, make it fixable, and sound right. Here is the anatomy of a message that does all three.
Self-studyGroup 1 - say what happened4 min▶
The reader cannot fix what they cannot see. Four rules make the failure legible:
- Identify the cause. State what went wrong and why, not just that something failed. "bad header" tells the reader nothing; "missing column: value" tells them everything.
- Identify the invalid input. Echo the exact bad value back. If the reader passed
event_type "click", put "click" in the message so they see it without hunting. - Specify requirements and constraints. State the valid range, format, or set. "valid types: page_view, signup, purchase, api_call" turns confusion into a checklist.
- Provide a corrected example. Show a line of valid input next to the rejected one. Examples teach faster than rules.
Self-studyGroup 2 - make it fixable4 min▶
A message that describes a problem but not its cure leaves the reader exactly where they started, only angrier:
- Explain how to fix it. Pair every problem statement with the concrete next step. This is the single highest-value rule: "add 'click' to config.yaml > events.valid_types" beats any amount of diagnosis.
- Be concise. The reader is frustrated. Trim to essentials; a wall of text at the error moment gets skipped.
- Avoid double negatives. "Input is not invalid" makes people re-read. Say what IS required.
- Format for readability. Progressive disclosure (headline first, detail below), and keep the message near the source of the error so the reader is not scrolling to connect them.
Self-studyGroup 3 - sound right4 min▶
Tone is not decoration - the wrong tone actively slows the fix:
- Write for the target audience. A message a developer reads in a log can be terser and more technical than one an analyst sees in a UI.
- Use terminology consistently. If config calls them "event types", the error says "event type", not "event kind". Same concept, same word, everywhere.
- Set the right tone. Be positive: "Enter a name" not "You didn't enter a name". Do not over-apologize - skip "sorry" and "please". Avoid humor; it lands badly mid-crisis. And never blame the reader: "The printer is offline" not "You specified an offline printer".
Rewrite ingest.py's message ★ 15 min · everyone follows
Pulse's ingest.py prints a real, weak message when a CSV has the wrong columns. Run it through the 13 rules and watch it get useful.
Cause named? "header is missing required columns" says what and why. Rule 1, done.
Invalid input echoed? The found line shows exactly what arrived, so the reader sees the gap without opening the file.
Constraint stated? The required line is the full valid header - a checklist, not a guess.
Fix given? Two concrete next steps, ordered most-likely first.
Break the silence in clean.py ★ 15 min · build your own
The harder fix. clean.py drops rows silently - the worst error-handling sin, because there is no message to improve, only an absence to fill. Give it a voice.
Audit every print and stderr write across ingest.py, clean.py, and metrics.py. List the silent drops too - the missing messages.
Classify each against the 13 rules: which band is missing - what happened, the value, the constraint, or the fix?
Rewrite each. For silent failures, decide the summary line: counts kept and dropped, grouped by reason.
Document the new output as a contract in docs/reference-config.md: readers should be able to rely on the drop summary, so it belongs in the reference, not just the code.
The summary line that ended a standup topic. "Why is today's number low?" was a standing agenda item on one data team until clean.py started printing a kept/dropped summary per run. The answer was now in the logs before anyone asked. The question left the standup for good.
Try it yourself ◐ 30-45 min
- Grep your own codebase for generic messages: "error", "failed", "invalid", "something went wrong". Pick the three highest-traffic ones and run them through the four bands.
- Find one place your code fails silently (a dropped row, a swallowed exception, a default that hides a problem) and give it a voice.
- Apply the blame test to every user-facing message in one screen or CLI: does any of them make the reader feel accused? Rewrite to describe the state, not the reader's mistake.
- Optional depth: Google's Writing Helpful Error Messages course is free at developers.google.com/tech-writing/error-messages, including a "want to play a game?" rewrite exercise.
Official sources covered
This deep dive teaches the working content of Google's 16-unit error-messages course, applied to Pulse. Back-end-specific guidance is summarized; the original goes deeper on API error models.
Three questions before you go 🎯 ◐ 90 seconds
1 · clean.py drops invalid rows and prints nothing. What sin is that?
Silent failure hides the problem entirely. The fix is a drop summary: counts kept and dropped, grouped by reason.
2 · Which message sets the right tone?
Positive framing, no blame, no over-apology, no humor mid-crisis. State the constraint and let the reader act.
3 · A message says "event_type is not valid". What's the highest-value thing to add?
Echoing "click" plus "add it to config.yaml > events.valid_types" covers two of the four bands the reader most needs.