Skip to content

Errors & returns

About

Errors in blombly are special values that can be found in place of any computation outcome. You can identify and handle them in your code with the as and catch statements mentioned below. Ignore this option if you are only interested in your application's "happy path". Unhandled errors cascade to dependent code, including within returned values from functions and methods.


Most errors do not immediately terminate functions. This gives you a window to handle them. For example, operations like converting invalid strings to numbers, or using the next operator of iterators may return errors in certain situations that you can handle. The few cases where functions are immediately terminated comprise attempts at modifying invalid data, such as pushing to a non-existing variable, or trying to set error values to struct fields, list elements, etc. In those cases, errors are directly returned; catch those either at one level higher or with the do clause shown at the bottom of this page.


Consider the following code in which we provide invalid console inputs. The function add has float on its arguments, which means that they will be subjected to the conversion x|=float;y|=float;. This is injected during compilation. As already mentioned, an error that occurs upon this conversion does not halt the function's execution. Blombly is well-behaved in such scenarios, computing what it can.

// main.bb
add(float x, float y) = { // converts arguments to floats
    print("Trying to add: !{x} and !{y}");
    print("This will always appear.");
    return x+y;
}
x = "First number:"|read;
y = "Second number:"|read;
z = add(x, y);
catch(z) print("Operation failed");
> ./blombly main.bb
First number: 1
Second number: foo
( ERROR ) Failed to convert string to float
     {print("Trying to add: "+str(x)+" and "+            main.bb line 1
This will always appear.
Operation failed.

Tip

The main error handling pattern is to check whether a function returns an error.

Error handling

One way of handling errors is through the as keyword. This is similar to assignment with =, but also returns a bool that indicates whether an error was avoided or encountered (true or false respectively). Below is a simple one-liner that retries reading from the console until a number is provided.

// main.bb
while(not number as "Give a number:"|read|float) {}
print(number);
> ./blombly main.bb
Give a number: number
Give a number: 12
12

Catch errors without assigning them eslewhere with the catch(@expression) {@found} else {@notfound} pattern. This has identical syntax to conditional statements, so you can skip the brackets or the alternative clause. Use it to check that specific values are not errors. This includes testing for non-missing variables. The snippet below demonstrates how the default statement is implemented by the standard library by catching whether the namesake variable would be an error.

// main.bb
inc(x) = {
    catch(bias) bias = 1;
    return x + bias;
}
print(inc(0));
print(inc(0 :: bias=2));
> ./blombly main.bb
1
2

Failing

You can deliberately create errors using the fail keyword. This accepts a string message and causes currently executed functions or methods to halt immediately and return an error. Below is an example.

final counter = new {
    \value = 0; // private
    inc() = {this\value += 1}
    int() => this\value;
}

final safediv(x,y) = {
    if(y==0) fail("Cannot divide by zero");
    counter.inc();
    return x/y;
}

if(result as safediv(1,2)) print(result);
if(result as safediv(1,3)) print(result);
if(result as safediv(1,0)) print(result);

print("Number of safe divisions: !{counter|int}");
> ./blombly main.bb
0.500000
0.333333
Number of safe divisions: 2

The assert @condition; statement is a macro shipped with the language that fails when a condition is not met without any error message. The macro's implementation is if(@condition) fail(!stringify(@condition)); and you can use it for quick checks. In practice, create assertions whenever you are trying to prevent operations that would affect hidden state from taking place if computations that folllow them would fail.

Do-return

The @result = do{@code} pattern intercepts return statements. We left it for here because it also intercepts errors that would cause functions to fail. When entered and exited, this pattern also waits for parallel function calls to conclude and applies all defer statements. Omit brackets when only one command is tried. You may also not assign the result anywhere.

Tip

Think of do as a function call that affects the scope.

For example, let the interception mechanism interrupt control flow like this sgn = do if(x>=0) return 1 else return -1;. A similar syntax breaks away from loops below. Contrary to errors, returning is lightweight to intercept. You could also prepend do to loop bodies to let internal returns skip the rest of the body - this emulates other languages' continue just as the syntax below emulates break. Blombly does not have extra keywords to enforce only one way of interrupting execution.


Use catch to check whether do intercepted a non-error ereturn; not intercepting any return is also considered an error. An example follows.

// main.bb
start = int("start:"|read);
end = int("end:  "|read);
i = 0;
result = do while(i <= end) {
    i = i + 3;
    if(i >= start) return i;
}
print("Finished searching.");
catch(result) fail("Found nothing: !{result}"); // creates a custom error on-demand
print("The least multiple of 3 in range [!{start}, !{end}] is: !{result}");
> ./blombly main.bb
start: 4
end:   100
Finished searching. 
The least multiple of 3 in range [4, 100] is: 6 

> ./blombly main.bb
start: 4
end:   1
Finished searching. 
( ERROR ) Found nothing: No error or return statement intercepted with `do`.
     fail("Found nothing: "+str(result|str)+"            main.bb line 9
     fail("Found nothing: "+str(result|str)+"            main.bb line 9
     catch(result)fail("Found nothing: "+str(            main.bb line 9