learn-tech-writing-with-phoebe / Deep dive 1 of 4
Learn Tech Writing with Phoebe · Deep dive 1 of 4

Writing helpful error messages

Error messages are the most-read documentation you will ever write, and they get read at the worst possible moment: when something is already broken. Google has a whole course on them. Here it is, applied to Pulse's real errors.

🟣 Deep dive Self-paced Engineers & data teams 60-90 min self-study
0-10 · Rules 10-40 · The 13 message rules 40-75 · Rewrite workshop 75-90 · Checklist
Part 0

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.

Self-study - read at your pace ★ Try it now exercise Source: Google Error Messages course
★ What you walk out with The 13 rules for a helpful message, one memorable anatomy diagram, and a rewritten set of Pulse's actual error output - including turning clean.py's silent row-dropping into a message that tells the reader exactly what it skipped and why.
Part 1 · covers general error-handling rules

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.
Real world

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.
Part 2 · covers the 13 message rules

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.

clean.py: skipped 2 rows in events_2026-07-01.csv reason: event_type "click" is not valid valid types: page_view, signup, purchase, api_call fix: add "click" to config.yaml > events.valid_types What happened skipped 2 rows, named file The bad value echoes "click" back The constraint lists valid types The fix exact next step Four bands, every time: what broke, the offending value, the rule, the action. Miss one and the reader is stuck.
🔍 Click to zoom - the four bands every good error message carries
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".
Writing tip The blame test: read your message aloud and ask "does this make the reader feel stupid?" "You entered an invalid date" quietly accuses. "Use the format YYYY-MM-DD" just helps. Same information, opposite feeling.
Workshop 1 of 2

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.

ingest.py's message, before and after the 13 rules ✗ Error: bad header "bad header" says nothing about why sends reader to source at 2am ✓ Missing columns, named header is missing required columns shows the exact header found two fixes, most likely first Same failure, four missing rules added: cause, the bad value, the constraint, and the fix.
🔍 Click to zoom - the same failure, rewritten with the 13 rules working
What ingest.py prints todayskip events_2026-07-01.csv: bad header
After the 13 rulesingest.py: skipped events_2026-07-01.csv - header is missing required columns. found: event_id, user_id, ts, value missing: event_type required: event_id, user_id, event_type, ts, value fix: add the missing column to the CSV header, or check the export that produced this file.

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.

Data team tip Notice the message names the file. In a pipeline that ingests a folder of CSVs, "bad header" forces the reader to guess which file; naming it turns a hunt into a fix.
Workshop 2 of 2

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.

clean.py: from silent drop to a summary line ✗ Silent row drop clean.py drops bad rows no print, no log, no trace metrics quietly wrong ✓ Kept/dropped summary counts kept and dropped grouped by reason documented in reference-config.md The worst error-handling sin has no message to fix, only an absence to fill with one summary line.
🔍 Click to zoom - an absence needs a summary line, not silence
clean.py today - says nothing# rows with invalid event types or bad timestamps are dropped. # no message. metrics are quietly wrong. reader has no idea.
A per-file drop summaryclean.py: events_2026-07-01.csv - kept 8 rows, dropped 2. dropped 2: event_type not in valid_types (saw "click") dropped 0: unparseable timestamp valid types come from config.yaml > events.valid_types to keep an event type, add it there and re-run.

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.

Real world

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.

Homework

Try it yourself ◐ 30-45 min

Source material

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.

✓General error handling rulesPart 1: fail loudly, keep the cause, raise immediately
✓Identify cause, inputs, requirements, examplesPart 2 group 1: say what happened
✓Explain the fix, be concise, no double negatives, formatPart 2 group 2: make it fixable
✓Audience, consistent terminology, tonePart 2 group 3: sound right
◐Additional guidelines for back-end engineersSummarized; full API-error detail stays with the original course
Check yourself

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.

Error-message cheat sheet · pin this

Four bandsWhat happened · the bad value · the constraint · the fix. Every message, every time.
Never fail silentlyA dropped row with no message is a bug you'll debug for hours. Print a kept/dropped summary.
Keep the causeDon't flatten a specific failure into generic "error". The real reason is right there - surface it.
Echo the inputPut the offending value in the message. "saw 'click'" beats "invalid type".
Pair problem with fixThe single highest-value rule. Every problem statement gets a concrete next step.
Be conciseThe reader is frustrated. Trim to essentials; detail below the headline.
Don't blame"The printer is offline", not "You specified an offline printer". Describe state, not fault.
No sorry, no jokesSkip apologies and humor mid-crisis. Consistent terminology, positive framing.