I have a pretty cool calendar in my cubicle at work. Every month it shows me a different software development anti-pattern, and has a clever little picture to help me remember them. I’ve already ordered next year’s.
Earlier this year, the calendar had a picture of a doctor writing “Diagnosis = Sick” on a clipboard, and an admonition to write descriptive error messages. I started wondering, “Why is it hard to write descriptive error messages?” One explanation is that the curse of knowledge. The curse of knowledge basically says that if I’m trying to guess how other people behave, I will accidentally bias the guess with my knowledge. That is, I can imagine how I would act in different circumstances, but I can’t how I would act if I knew different things.
Writing error messages, of course, runs square into the curse of knowledge: When I write an error that my program will display, I, by definition, know what is wrong, because I know the state the program must be in display that error. Because I know what’s wrong, I am of course able to interpret more or less any sentence I compose as an error message to mean what I think it will mean. It is very difficult for me to guess how someone else will interpret that error message. They might find it perfectly lucid, or it might be immensely baffling; I can’t easily imagine seeing the error message while not knowing what it means.
On the other hand, I think some exceptions are inherently more explanatory than others: Let’s take C#’s IndexOutOfRange exception. You had an array of 5 things, you asked for the 6th thing, boom! IndexOutOfRange. It’s super easy to interpret because it can only mean one thing. Other error messages that seem super easy to interpret are, in no particular order:
ArgumentNullException: This method needs an object, you gave it a null pointer instead. Shame on you.
KeyNotFoundException: There was a collection of keys and values. You asked for the value associated with some key that’s not present in the collection.
- Perhaps clearest of all:
DivideByZeroException. Turns out C# can’t do that because, well, math can’t do that.
What do these exceptions have in common? Well, for one thing, they’re all boneheaded exceptions. What makes them boneheaded exceptions? They’re the programmer’s fault. If you’re dividing two numbers, you could always check to see whether the divisor is zero before preceding. If you’re accessing an array, you really ought to know what size it is before you ask for the nth element, etc. Maybe we can call them boneheaded because you’re violating an assumption that the program has to make. If you’re asking for the nth element in the array, obviously the underlying code has to assume that the array contains at least n elements. If you’re dividing numbers, obviously the program has to assume that the divisor isn’t zero.
Some exceptions inform the consumer of the assumption they’ve violated. The
ArgumentNullException is a good example. Maybe I thought I was allowed to pass in a null value to that method, or, more likely, maybe I thought the argument that I’d passed in wasn’t null. The programmer who threw the argument null exception is telling me: “Look, when I wrote this method, I assumed that this argument was going to be a real object, OK?”
We seem to have two types of exceptions, then: Exceptions that are obvious because they contain thee assumption necessary for the command to be logical (e.g., divisors can’t be zero) or exceptions that are obvious because they tell you what assumption you’ve violated.
Exceptions get harder to diagnose as the assumption that’s been violated gets less clear. For example, a null reference exception just means “You assumed some object wasn’t null, because you accessed one of its members, but actually it was null.” This exception is harder to diagnose because it’s not declared in your code which objects you are assuming not to be null.
Perhaps now we can formulate a general rule for error messages: Error messages are helpful if they tell you what assumption you’ve violated. File not found? Obvious: violating the assumption that there was a file at that path. Argument null exception? Obvious: violating the assumption that the argument was not null.
The other day I lost some time to an invalid operation exception. It was a little tricky to diagnose, because it basically says: You thought you were allowed to call that method while the object was in the state it was in, but you were wrong. This is a bad error message because it doesn’t tell me the precondition I’d failed to meet. The error would have been much more helpful if it had said, “You need to call
.Initialize() before you call
.Execute(),” for example. In other words, it should have told me the difference between what the person who wrote the code that threw the exception thought would be true, not what I thought would be true. Telling me I’ve performed an invalid operation tells me nothing I didn’t know: You tried to call this method and the call failed.
So here’s my rule for writing error messages: Tell the consumer what assumptions about the state of the program you had to make in order for that command to succeed, and which ones weren’t true. Instead of saying, “Invalid input,” say “That field must contain an email address.” Saying “invalid input” tells me that my input is invalid, which I already knew just by the fact that there was an error. Telling me “that field must contain an email address” tells me that you, the author of the program, had to assume that I, the user of the program, would type an email address, but I didn’t. It gives me the information you have that I lack, so it helps me.
In other words, we can say this:
All error messages should report what assumption about the state of the program was violated to cause the error. They must always report assumptions made by the implementer and violated by the consumer, rather than assumptions made by the consumer, since telling the consumer what assumptions they’ve made doesn’t help them.
So, the next time you’re writing a part of a program that might throw an exception, ask yourself, “what assumptions did I make whose violation led to this error?”
Also, if you have a favorite example of an incomprehensible error message, please share it in the comments.
Till next week, happy learning!