Nobody reads error messages until they need one. Then it’s the only thing that matters.

The 3 AM Encounter

Picture this: a deployment fails at an inconvenient hour. You’re staring at a terminal, tired, context-switched from whatever you were doing before. The system presents you with:

Error: operation failed (code 47)

Code 47. What does that mean? Is it a network problem? A permissions issue? Did the deployment script fail, or did the service crash after starting? You don’t know. The error message doesn’t know either, apparently.

Now imagine instead:

Error: deployment failed — health check timed out after 30s.
The service started but never responded on port 8080.
Check: is the PORT environment variable set? Last successful health check was 2h ago.

Same failure. Same moment. But the second message turns a mystery into a checklist. It tells you what happened, what was expected, and where to look next. That’s not just better formatting. That’s a fundamentally different relationship between the system and the person trying to fix it.

Errors Are User Interface

We spend enormous effort designing APIs, choosing function names, structuring response payloads, documenting endpoints. These are all interfaces — contracts between the system and its users. We review them, debate them, version them.

Error messages are also interfaces. They’re the interface for the failure case. And the failure case is when the user most needs clarity, because something has already gone wrong and they’re trying to recover.

Yet error messages are routinely written as afterthoughts. A developer writes the happy path, tests it, refines it. Then they add a catch block, throw in a generic message — “something went wrong,” “invalid input,” “unexpected error” — and move on. The error path gets the least attention at the moment when it needs the most.

What Good Error Messages Contain

After debugging countless failures across different systems, I’ve noticed that the most helpful error messages consistently contain the same elements:

What happened. Not a code, not a generic label. A description. “Connection to the database timed out after 5 seconds” is a description. “DB_ERR_TIMEOUT” is a code that requires a lookup.

What was expected. The system expected a response within 5 seconds and didn’t get one. It expected a JSON body and got XML. It expected a file at a specific path and the file wasn’t there. Stating the expectation makes the gap between “what should happen” and “what did happen” immediately visible.

What to try next. This is the element most error messages omit, and it’s the most valuable. “Check that the database host is reachable from this network.” “Verify the file permissions on /path/to/config.” “Run the diagnostic command to test connectivity.” You’re giving the user a next step instead of a dead end.

Context about the state. When did this last work? What input triggered the failure? What was the system trying to do when it failed? A stack trace is one form of context, but often the operational context — “this happened during the daily sync job, processing record 4,827 of 12,000” — is more useful for quick diagnosis.

The Empathy Principle

Writing good error messages requires imagining yourself in the user’s position at the moment of failure. What do they know? What don’t they know? What tools do they have available? Are they in a terminal or reading a log file? Are they the developer who wrote this code, or someone on an operations team who’s never seen the internals?

This is fundamentally an empathy exercise. The person reading this message is having a bad moment. Something broke. They might be under time pressure. They might be unfamiliar with this part of the system. The error message is your one chance to help them, and you’re writing it days or weeks or months before the moment arrives.

The best error messages I’ve encountered feel like they were written by someone who anticipated exactly this situation and left a note for the next person. “If you’re seeing this, here’s what probably happened and here’s what to do.” It’s a small act of kindness embedded in the code.

The Testing Blind Spot

Error paths are systematically undertested. The happy path gets unit tests, integration tests, end-to-end tests. The error path gets, at best, a test that verifies an exception is thrown. Rarely does anyone test that the exception message is actually useful.

I’ve started thinking about error message review as a distinct activity. Not just “does this function handle errors?” but “when this function fails, will the person debugging it have enough information to understand what went wrong?”

Read your own error messages. Imagine you’re encountering them for the first time, with no context about the code that produced them. Are they helpful? Or do they just confirm that something broke without explaining what or why?

The Compound Effect

Good error messages don’t just help in the moment. They compound over time.

A codebase with clear, informative error messages builds a culture of diagnostic clarity. New contributors learn what “good” looks like and replicate it. On-call engineers can resolve incidents faster because the system tells them what’s wrong. Fewer issues get escalated because the first person to see the error has enough information to act.

A codebase with vague error messages builds a different culture — one of institutional knowledge, where you need to “just know” what error code 47 means, where debugging requires reading source code rather than reading the error, where new contributors are slower to become effective because the system doesn’t help them.

The investment in clear error messages is small per instance. Write a sentence instead of a code. Include the expected value alongside the actual value. Suggest a next step. Each one takes seconds. Over hundreds or thousands of error paths, those seconds become the difference between a system that’s pleasant to operate and one that fights you at every turn.

Write error messages for the person who’ll read them at their worst moment. They deserve more than “code 47.”