Happy families are all alike; every unhappy family is unhappy in its own way.
Functions are similar in that way: a function succeeds in only one way, but it will fail in many ways. This is the first principle that guides the major design decisions around error handling in Leema. The second principle is that any function can fail.
When any function can fail, the result of a failed function starts to look a lot more like a primitive type in the language. There are two benefits from building the failure type into the language:
- No need to pollute function return types with specific failure types for that function
- Handling the failure will always be the same, regardless of the cause of the failure
Implicit Failure Types
Strongly typed languages have problems with failures types. A function should open a file, but it might not. What will happen if the file cannot be opened has to be declared. In Java, the declaration for Files.readAllBytes() includes that it might throw an IOException. But then if you read the comments you’ll see that it might also throw an OutOfMemoryError or a SecurityException. It might be easier to list the functions that cannot throw an OutOfMemoryError.
In Go, opening a file returns a 2 element tuple:
func Open(name string) (file *File, err error)
I’m sure this gets easier once you’re used to it, but it still takes extra thought to separate out the error type. Once every function can fail, the declaration of this error type becomes extraneous and can be made implicit.
Combining Failure Types
Declared failure types become even more complicated as they’re combined. Imagine a function, foo, that opens a network connection and reads data, then parses the data and returns the result. If the connection fails, we might want to recognize that and retry or report it one way. If the parse fails, that probably means we have an error somewhere, that retrying wouldn’t help and that we want to report differently. The network library reports errors one way, the parsing library another.
How do we declare the failure type of foo? Have it return 2 error types, one for the network failures and one for the parse failures? This doesn’t scale as our program grows and we have functions that can fail in 6 or 7 different ways. Or do we declare a new error type just for foo that represents all the possible ways that it can fail? This doesn’t scale either as the number of functions that can fail in multiple ways grows.
Here’s an example of how Leema approaches this problem:
func fetch_employees(address): [Employee] -> let employee_text := fetch_employees_from_network(address) parse_employee_data(employee_text) --
Because the failure type is implicit, there is no pollution to the return type, which is simply a list of Employee objects. And because they are guaranteed to return the same failure type, the errors from the different libraries still aggregate into the same implicit failure type.