Error Handling

Claro takes a very principled stance that all control flow in the language should be modeled in a way that is self-consistent within the type system - as such, Claro chooses not to model errors around "throwing Exceptions". While many languages (e.g. Java/Python/C++/etc.) were designed around thrown exceptions as their error modeling tool, they all suffer from the same antipattern that make it impossible to determine strictly from looking at a procedure signature whether it's possible for the call to fail, and if so, what that failure might look like. This leads users into unnecessary digging to read implementation details to determine how and why certain unexpected error cases inevitably arise.

So, taking inspiration from many prior languages such as Rust, Haskell, and Go, Claro requires errors to be modeled explicitly in procedures' signatures as possible return types so that all callers must necessarily either handle any potential errors, or explicitly ignore them or propagate them up the call stack.

std::Error<T>

Claro's std module exports the following type definition:

Fig 1:


newtype Error<T> : T

This type is a trivial wrapper around any arbitrary type. Its power is in the special treatment that the compiler gives to this type to power Claro's error handling functionality. But first, let's take a look at how a procedure might make use of this type to represent states in practice - the below example demonstrates a function that models safe indexing into a list:

Fig 2:


atom IndexOutOfBounds
function safeGet<T>(l: [T], i: int) -> oneof<T, std::Error<IndexOutOfBounds>> {
  if (i < 0 or i >= len(l)) {
    return std::Error(IndexOutOfBounds);
  }
  return l[i];
}

var l = [1, 2, 3];
match (safeGet(l, getRandomIndex())) {
  case _:std::Error<IndexOutOfBounds> -> print("Index out of bounds!");
  case X                              -> print("Successfully retrieved: {X}");
}
# ...
provider getRandomIndex() -> int {
  random::forSeed(1)
    |> random::nextNonNegativeBoundedInt(^, 8)
    |> var i = ^;
  return i;
}

Output:

Index out of bounds!

To drive the example home, instead of wrapping an atom which doesn't provide any information beyond the description of the error itself, the error could wrap a type that contains more information:

Fig 3:


atom TooHigh
atom TooLow
newtype IndexOutOfBounds : struct {
  reason: oneof<TooHigh, TooLow>,
  index: int
}
function safeGet<T>(l: [T], i: int) -> oneof<T, std::Error<IndexOutOfBounds>> {
  if (i < 0) {
    return std::Error(IndexOutOfBounds({reason = TooLow, index = i}));
  } else if (i >= len(l)) {
    return std::Error(IndexOutOfBounds({reason = TooHigh, index = i}));
  }
  return l[i];
}

var l = [1, 2, 3];
match (safeGet(l, getRandomIndex())) {
  case std::Error(ERR) ->
    var unwrappedErr = unwrap(ERR);
    match (unwrappedErr.reason) {
      case _:TooHigh ->
        print("Index {unwrappedErr.index} is too high!");
      case _:TooLow ->
        print("Index {unwrappedErr.index} is too low!");
    }
  case X -> print("Successfully retrieved: {X}");
}
# ...
provider getRandomIndex() -> int {
  random::forSeed(1)
    |> random::nextNonNegativeBoundedInt(^, 8)
    |> var i = ^;
  return i;
}

Output:

Index 5 is too high!

Continue on to the next section to learn about how Claro enables simple propagation of std::Error<T> values.