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-
The correct approach to using the return value is to get rid of
null and use a separate type (usually called
Maybe) that represents the concept of "a value of type
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.