Dual approach to error handling
Boomla supports two approaches to error handling.
Implicit error handling optimizes for the happy path. Upon failure, errors are implicitely propagated up in the call chain, thus the developer experience is similar to that of throwing an exception.
This approach produces cleaner looking code at the cost of poorer understanding of failure paths in your program. It may be a great choice for prototypes or when programming in a layer where errors would always be just passed on to the callee. It's also great when you are just hacking together something for yourself.
Explicit error handling requires every error to be explicitly handled. You need to actively consider handling the error even if it still means just passing it on to the callee.
Use this approach to build robust software that handles failure cases well.

Compatibility

To make the two worlds compatible, Boomla requires that all function signatures contain errors, both in the case of explicit and implicit error handling.
As the type checker has complete information about error paths at compile time, it can type check errors and detect call sites where errors can not be propagated.

Choose locally

It is decided at the function level whether it uses implicit or explicit error handling internally. It is implicity by default. To turn on explicit error handling, add a ? after the function signature.
To support implicit error propagation, the error result is separated in function signatures from the success results (output) to enable the compiler to understand which is which.
For completeness, there is a third scenario: every function may panic when encountering a developer error, alas a bug. Hence the result of a function call is one of these three values: an output (or success result), an error (or failure result), or a panic.
The return statement is used to return success results.
The fail statement is used to return a failure result. Use for client errors.
The panic statement is used to panic upon developer errors. As any function can panic, you can think of it as implicitely part of every function signature.

Examples

Let's see a few examples. We will write a main function that greet's a person and prints it to the console. To shorten the examples, they all depend on the following greet function:
import "errors" fn greet() (string) (error) { fail errors.New("not greeting today") }
Let's start with an example that uses implicit error handling. Notice that the greet() function may fail. If that happens, the error is implicitly propagated. That's valid because the greet function's error type can be assigned to the main function's error type.
fn main() () (error) { s := greet() // <-- may fail! println(s) }
Let's jump to implementing the same with explicit error handling. Notice the ? in the signature, which tells the compiler that every possible error result must be explicitely handled. Also note the question mark in the call greet()?. Here it is used to propagate the error, should the call fail.
fn main() () (error)? { s := greet()? println(s) }
The above is just a shorthand to the following error handler block. Here the failure result is assigned to the variable err, then it is propagated to the callee.
fn main() () (error)? { s := greet() err { fail err } println(s) }
Error handler blocks can be used even with implicit error handling, so this example would be valid as well. Notice that there is no ? after the function signature.
fn main() () (error) { s := greet() err { fail err } println(s) }
With explicit error handing, the compiler will fail when encountering unhandled errors.
fn main() () (error)? { s := greet() // <-- unhandled error println(s) }
With implicit error handling, the compiler will fail if an error can not be implicitly propagated:
fn main() { s := greet() // <-- error can not be propagated println(s) }