I wouldn't call null
and checked exceptions dual concepts; in fact, they're kind of the same thing in disguise (more on this later). Signaling failure in a return value and unchecked exceptions are dual concepts. Returning null
is one way to signal failure in the return value, but it subverts the language's type system. You've already caught on to that fact - you can never have the type of just Strings, you must live with the type of Strings-and-null
.
The correct approach to using the return value is to get rid of null
and use a separate type (usually called Option
or Maybe
) that represents the concept of "a value of type T
, or Nothing
". An Option X
is not an X
- you can't assign one to the other, and you have to explicitly inspect the Option
and handle both cases. You can't simply forget that the function could fail to return a value.
Maybe
works well when failures are common and can be handled immediately. It doesn't work so well when failures are rare and the immediate caller can't do anything meaningful about it. When failure can only be handled further up the stack, you have to return Maybe
all the way up - if you get a value, proceed, and if you don't, return Nothing
, over and over again until you get to the point where you can actually handle the error. Imagine if division didn't throw an exception and instead returned Maybe Double
- what a pain!
Not only is this clumsy, it forces everything between the handler and the error to know about the possibility of failure and change its return type to Maybe
. This gets in the way when you're writing functions that take other functions as arguments. Sometimes you want to pass a function that could theoretically fail, but you've either ensured this won't be the case, or you can't do anything about it if it did. Unchecked exceptions come to the rescue here, because they do the forwarding of the error automatically and anything in between can be blissfully ignorant of the possibility of failure. If your only option were Maybe
and checked exceptions, you'd be out of luck, because the possibility of failure is encoded in the function's return type and that makes it incompatible with the type of function you're expecting.
The problem with checked exceptions is that they're not fit for either purpose. When the error has to be handled further up, you force all code between the handler and the failure to reflect the possibility of failure in its signature. When the error can be handled immediately, you get all the benefits of Maybe
- you can't forget to handle it - but it's clumsier to use than a simple if/switch! So it's kind of a clumsy Maybe
in exception's clothings.