Always Return the Same Thing
It’s a cliche that the answer to any given programming question is “it depends” and hard rules that you should 100% follow should always be questioned as there is usually an exception somewhere. This means when I do have a rule I feel I should follow 100% of the time it’s worth special notice.
In the spirit of RFC 2119.
All functions or methods you write MUST return an identical type regardless of their success or failure.
The Ruby standard library often doesn’t follow this principal and it can cause problems. Looking at the Hash#[] method for example
hash = { answer: 42 }
hash[:answer] # => 42
hash[:other_key] # => nil
If a default value for the hash hasn’t been set, we return nil
instead of the value we wanted.
For this specific reason I will always use Hash#fetch which will either throw a KeyError
exception or allows me to return a default value.
This prevents our application from leaking a nil
into our program which can result in a NoMethodError
when we try to use that nil
in a context where one was not expected.
Avdi Grimm wrote a whole book on dealing with this and similar problems that is worth a look.
This isn’t just a Ruby problem, Java libraries will often return null
as an error which can result in the common NullPointerException
when you forget to check.
If it’s Ruby’s nil
, Java’s null
, or something similar, we are really talking about Tony Hoare’s billion dollar mistake.
Many languages have their own way to deal with this idiomatically.
Go functions will return a tuple containing the expected result and an error object that you must inspect to address this.
Rust’s standard library makes heavy use of the Option
or Result
types for when there may be no result or an error occurs.
In Ruby I’ll frequently use the dry-monads gem to provide a similar interface without having to write the boilerplate myself.
Whenever the result of a function or method is uncertain, you need to return a type that models that uncertainty.
Expecting someone, even yourself, to always remember to check for a nil
is guaranteed to result in problems eventually.
What about exceptions?⌗
I’m ignoring exceptions for the moment except to say that exceptions for error handling I feel is fundamentally flawed as it introduces an alternative control flow which complicates the application logic. If you have a truly unrecoverable error exceptions may give you a semantically appropriate way to end the program but if you think that the error could be handled, it should be modeled appropriately allowing the developer calling your function to handle the result in a consistent way.