Error handling in Leema is not done via exceptions or return codes, but rather something that is somewhere in between.
Leema failures are language-level data structures that are implicitly returnable by all functions. They are assigned to variables just like return codes and automatically propagate like exceptions.
One problem with error types in a statically typed language is that they often compose poorly. If we have a function that checks memcached for some data and if it’s not found, queries a database, our new function needs to be able to return the error type from the memcache library and from the database library. How does our fetch_users function define its return type?
In Leema, all functions return the same error type and because there’s only one error type, it can be implicitly defined for all functions. So no matter which libraries we’re mixing, all failures will be composable and handled in the same way.
Another benefit about having one universal, language-aware error type is that it can make code more readable. One problem with exceptions is that handling them clutters up our code and makes it hard to track what we were trying to do in the first place.
Imagine a function that gets some data with a series of sequential calls like this:
func get_data:Str :: input:Str -> let a := foo(input) let b := bar(a) baz(b) --
It’s easy to read right now, but it also has no error handling. Here’s what this leema function looks like once we add the error handling:
func get_data:Str :: input:Str -> let a := foo(input) ## a is valid here, or would have propagated let b := bar(a) ## b is valid here, or would have propagated baz(b) failed a |#some_error -> default_value_for_a() -- failed b |#another_error -> log("bar failed for: $a") refail(b) -- --
The top of the function looks exactly the same. The error handling can be written down below so the primary logic of the function is uncluttered by cases. In this case, if foo() fails, a default value can be used instead. But if bar() fails, then critical data is missing and the failure should be logged and propagated back down the call stack.
Because the Leema knows the difference between failure structure and regular data, it can do most of the checking for you. And it also knows to reorder your code so that, despite being written down below, the error checking happens immediately following assignment. So a can be passed directly to bar() and b passed to baz() and they will always be valid values. If they held failure objects, the function would have returned the failures before ever getting to the next statement.