| I agree that both exceptions and error values (aka result types) have their place. I would say that error values are good for when a caller should explicitly handle that case, and that exceptions are good for errors that a caller should not be expected to handle explicitly. A lot of times this breaks down as meaningful application errors vs operational or programming errors. I am struggling to find the right words for this, so I can give an example: Let's say we have a function used to register a new user account on a site like HN. An error value would be appropriate to return when the username is already taken, so that we can express to the caller that this is a possibility that must be handled. Most likely the caller would want to tell the user. A maintainer doesn't really care when this occurs, since it's part of the application's healthy behaviour. An exception would be appropriate if the database is unavailable. The caller would not be expected to tell this to the user, nor is there any logical way for the caller to react to this situation specifically. In this example of a web app, the best course of action is likely returning a generic "unexpected error" message and/or a HTTP 500. The caller can typically let the exception propagate to the web layer's top level exception handler where it will be logged. As a maintainer of the system, a stacktrace is valuable for pinpointing the problem with the code path that lead to it. (Checked exceptions, where available, blur these lines a bit) --- In the Java world... (stop reading if you don't care about Java) ...it has been increasingly common to see types like Result<T,E> used for error values. Recently, there have also been additions to the language that make errors-as-values more practical. Sealed classes (a preview feature in Java 16, and a full feature in the soon-to-be-release Java 17) are basically an implementation of product types (with a characteristically verbose Java-ey syntax) that could be used to implement results. Returning to our example with this: sealed interface RegistrationResult {
record Registered(Account newAccount) implements RegistrationResult { }
record UsernameTaken() implements RegistrationResult { }
...
}
https://openjdk.java.net/jeps/409beyond Java 17, you will be able to pattern-match over these with exhaustiveness enforced by the compiler. It will look something like: switch(registrationResult) {
case Registered(Account newAccount) -> ...;
case UserNameTaken() -> ... ;
...
}
https://openjdk.java.net/jeps/405 |