Typed Error Best Practices

Hi, I was wondering what the current best practices when using typed errors in your codebase. I currently use one error type per module which holds an integer to discriminate between variants. I then have implicit conversion for module error types that should be compatible. But somehow I still have problems with type inference / conversion. Here are some examples of what I am trying to do:

from std.builtin.globals import global_constant


@fieldwise_init
struct ErrorA(Equatable, ImplicitlyCopyable, Writable):
    var _variant: Int

    comptime UNKNOWN = ErrorA(_variant=0)
    comptime foo = ErrorA(_variant=1)

    @always_inline
    def variant_name(self) -> StaticString:
        comptime ERROR_A_VARIANT_NAMES: InlineArray[StaticString, 2] = [
            "unknown",
            "foo",
        ]
        ref global_variant_names = global_constant[ERROR_A_VARIANT_NAMES]()
        return global_variant_names[self._variant]

    @always_inline
    def msg(self) -> StaticString:
        comptime ERROR_A_VARIANT_MESSAGES: InlineArray[StaticString, 2] = [
            "Unknown error.",
            "Foo!",
        ]

        ref global_variant_messages = global_constant[
            ERROR_A_VARIANT_MESSAGES
        ]()
        return global_variant_messages[self._variant]

    def write_to(self, mut writer: Some[Writer]):
        writer.write("ErrorA.", self.variant_name(), ": ", self.msg())

    @implicit
    def __init__(out self, error_b: ErrorB):
        self._variant = error_b._variant


@fieldwise_init
struct ErrorB(Equatable, ImplicitlyCopyable, Writable):
    var _variant: Int

    comptime UNKNOWN = ErrorB(_variant=0)
    comptime bar = ErrorB(_variant=1)

    @always_inline
    def variant_name(self) -> StaticString:
        comptime ERROR_B_VARIANT_NAMES: InlineArray[StaticString, 2] = [
            "unknown",
            "bar",
        ]
        ref global_variant_names = global_constant[ERROR_B_VARIANT_NAMES]()
        return global_variant_names[self._variant]

    @always_inline
    def msg(self) -> StaticString:
        comptime ERROR_B_VARIANT_MESSAGES: InlineArray[StaticString, 2] = [
            "Unknown error.",
            "Bar!",
        ]

        ref global_variant_messages = global_constant[
            ERROR_B_VARIANT_MESSAGES
        ]()
        return global_variant_messages[self._variant]

    def write_to(self, mut writer: Some[Writer]):
        writer.write("ErrorB.", self.variant_name(), ": ", self.msg())


@fieldwise_init
struct ContextManager(ImplicitlyCopyable):
    var name: StaticString

    @always_inline
    def __enter__(mut self):
        print("Entering ", self.name)

    @always_inline
    def __exit__(mut self):
        print("Exiting ", self.name)

    @always_inline
    def __exit__[ErrType: Copyable](mut self, err: ErrType) -> Bool:
        self.__exit__()
        return False


def foo() raises ErrorA:
    raise ErrorA.foo


# Unexpected compiler error: return expected at end of function with results
def bar() raises ErrorB -> Bool:
    with ContextManager(name="bar"):
        raise ErrorB.bar
        return False


def foobar(call_foo: Bool) raises ErrorA:
    with ContextManager(name="foobar"):
        if call_foo:
            foo()
        else:
            _ = bar()


def main() raises:
    try:
        with ContextManager(name="main"):
            foobar(True)
            foo()
            bar()
    except e:
        print(e)

Thanks already for taking your time and giving feedback :blush:

I had the same question and since no one has responded, I thought I’d pitch you an idea and you can evaluate it and maybe build on it.

I’m an experienced FPer / Scala programmer and in FP we use algebraic data types to model the domain.
In mojo we can use Variant type as sum type and struct as product type.

let’s say our domain error model for a checkout functionality is like this:

- AppError
  - BusinessLogicError
    - EmptyCartError
    - StockUnavailableError
  - ServiceClientError
    - StockServiceUnavailable(reason)
    - PaymentServiceError(reason)

it can be modeled in mojo like this. unfortunatelly Variant doesn’t nest from what I understand, so the nested error structure has to be flattened into a single layer variant:

from std.utils import Variant

@fieldwise_init
struct EmptyCartError(Copyable, Movable, ImplicitlyCopyable, Writable):
    pass

@fieldwise_init
struct StockUnavailableError(Copyable, Movable, ImplicitlyCopyable, Writable):
    pass

@fieldwise_init
struct StockServiceUnavailable(Copyable, Movable, ImplicitlyCopyable, Writable):
    var reason: String

@fieldwise_init
struct PaymentServiceError(Copyable, Movable, ImplicitlyCopyable, Writable):
    var reason: String

comptime AppError = Variant[EmptyCartError, StockUnavailableError, StockServiceUnavailable, PaymentServiceError]

def add_to_cart(items: List[String]) raises AppError:
    if len(items) == 0:
        raise EmptyCartError()
    print("Added", len(items), "items")


def checkout(cart: List[String]) raises AppError:
    add_to_cart(cart)
    if len(cart) > 5:
        raise StockServiceUnavailable("warehouse offline")
    print("Checkout complete")


def main() raises:
    try:
        checkout(["a", "b"])
    except e:
        print("Caught:", e)

    try:
        checkout([])
    except e:
        print("Caught:", e)

    try:
        checkout(["a", "b", "c", "d", "e", "f", "g"])
    except e:
        print("Caught:", e)