Recoverable Errors with Result

La mayoría de los errores no son lo suficientemente graves como para que el programa se detenga por completo. A veces, cuando una función falla, es por una razón que usted puede interpretar fácilmente y a la que puede responder. Por ejemplo, si intenta sumar dos enteros grandes y la operación se desborda porque la suma excede el valor máximo representable, es posible que desee devolver un error o un resultado envuelto en lugar de causar un comportamiento indefinido o terminar el proceso.

The Result Enum

Recall from Generic data types section in Chapter 8 that the Result enum is defined as having two variants, Ok and Err, as follows:

enum Result<T, E> {
    Ok: T,
    Err: E,
}

El enum Result<T, E> tiene dos tipos genéricos, T y E, y dos variantes: Ok que tiene el valor de tipo T y Err que tiene el valor de tipo E. Esta definición hace que sea conveniente usar el enum Result en cualquier lugar donde tengamos una operación que pueda tener éxito (devolviendo un valor de tipo T) o fallar (devolviendo un valor de tipo E).

The ResultTrait

El rasgo ResultTrait proporciona métodos para trabajar con el enum Result<T, E>, como desenvolver valores, comprobar si el Result es Ok o Err, y entrar en pánico con un mensaje personalizado. La implementación de ResultTraitImpl define la lógica de estos métodos.

trait ResultTrait<T, E> {
    fn expect<+Drop<E>>(self: Result<T, E>, err: felt252) -> T;

    fn unwrap<+Drop<E>>(self: Result<T, E>) -> T;

    fn expect_err<+Drop<T>>(self: Result<T, E>, err: felt252) -> E;

    fn unwrap_err<+Drop<T>>(self: Result<T, E>) -> E;

    fn is_ok(self: @Result<T, E>) -> bool;

    fn is_err(self: @Result<T, E>) -> bool;
}

Los métodos expect y unwrap se parecen en que ambos intentan extraer el valor de tipo T de un Resultado<T, E> cuando está en la variante Ok. Si el Resultado es Ok(x), ambos métodos devuelven el valor x. Sin embargo, la diferencia clave entre los dos métodos radica en su comportamiento cuando el Result está en la variante Err. El método expect te permite proporcionar un mensaje de error personalizado (como un valor felt252) que se utilizará cuando se produzca el pánico, dándote más control y contexto sobre el pánico. Por otro lado, el método unwrap entra en pánico con un mensaje de error por defecto, proporcionando menos información sobre la causa del pánico.

The expect_err and unwrap_err methods have the exact opposite behavior. If the Result is Err(x), both methods return the value x. However, the key difference between the two methods is in case of Result::Ok(). The expect_err method allows you to provide a custom error message (as a felt252 value) that will be used when panicking, giving you more control and context over the panic. On the other hand, the unwrap_err method panics with a default error message, providing less information about the cause of the panic.

A careful reader may have noticed the <+Drop<T>> and <+Drop<E>> in the first four methods signatures. This syntax represents generic type constraints in the Cairo language, as seen in the previous chapter. These constraints indicate that the associated functions require an implementation of the Drop trait for the generic types T and E, respectively.

Por último, los métodos is_ok y is_err son funciones de utilidad proporcionadas por el rasgo ResultTrait para comprobar la variante de un valor del enum Result.

  • is_ok toma una instantánea de un valor Result<T, E> y devuelve true si el Result es la variante Ok, lo que significa que la operación se ha realizado correctamente. Si el Resultado es la variante Err, devuelve false.
  • is_err takes a snapshot of a Result<T, E> value and returns true if the Result is the Err variant, meaning the operation encountered an error. If the Result is the Ok variant, it returns false.

These methods are helpful when you want to check the success or failure of an operation without consuming the Result value, allowing you to perform additional operations or make decisions based on the variant without unwrapping it.

Puede encontrar la implementación del ResultTrait aquí.

It is always easier to understand with examples. Have a look at this function signature:

fn u128_overflowing_add(a: u128, b: u128) -> Result<u128, u128>;

It takes two u128 integers, a and b, and returns a Result<u128, u128> where the Ok variant holds the sum if the addition does not overflow, and the Err variant holds the overflowed value if the addition does overflow.

Ahora, podemos utilizar esta función en otros lugares. Por ejemplo:

fn u128_checked_add(a: u128, b: u128) -> Option<u128> {
    match u128_overflowing_add(a, b) {
        Result::Ok(r) => Option::Some(r),
        Result::Err(r) => Option::None,
    }
}

Here, it accepts two u128 integers, a and b, and returns an Option<u128>. It uses the Result returned by u128_overflowing_add to determine the success or failure of the addition operation. The match expression checks the Result from u128_overflowing_add. If the result is Ok(r), it returns Option::Some(r) containing the sum. If the result is Err(r), it returns Option::None to indicate that the operation has failed due to overflow. The function does not panic in case of an overflow.

Let's take another example:

fn parse_u8(s: felt252) -> Result<u8, felt252> {
    match s.try_into() {
        Option::Some(value) => Result::Ok(value),
        Option::None => Result::Err('Invalid integer'),
    }
}

In this example, the parse_u8 function takes a felt252 and tries to convert it into a u8 integer using the try_into method. If successful, it returns Result::Ok(value), otherwise it returns Result::Err('Invalid integer').

Nuestros dos casos de prueba son:

fn parse_u8(s: felt252) -> Result<u8, felt252> {
    match s.try_into() {
        Option::Some(value) => Result::Ok(value),
        Option::None => Result::Err('Invalid integer'),
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_felt252_to_u8() {
        let number: felt252 = 5;
        // should not panic
        let res = parse_u8(number).unwrap();
    }

    #[test]
    #[should_panic]
    fn test_felt252_to_u8_panic() {
        let number: felt252 = 256;
        // should panic
        let res = parse_u8(number).unwrap();
    }
}


Don't worry about the #[cfg(test)] attribute for now. We'll explain in more detail its meaning in the next Testing Cairo Programs chapter.

#[test] attribute means the function is a test function, and #[should_panic] attribute means this test will pass if the test execution panics.

The first one tests a valid conversion from felt252 to u8, expecting the unwrap method not to panic. The second test function attempts to convert a value that is out of the u8 range, expecting the unwrap method to panic with the error message Invalid integer.

The ? Operator

El último operador del que hablaremos es el operador ?. El operador ? se utiliza para un manejo de errores más idiomático y conciso. Cuando usas el operador ? en un tipo Result u Option, hará lo siguiente:

  • If the value is Result::Ok(x) or Option::Some(x), it will return the inner value x directly.
  • If the value is Result::Err(e) or Option::None, it will propagate the error or None by immediately returning from the function.

El operador ? es útil cuando se desea manejar los errores implícitamente y dejar que la función de llamada se ocupe de ellos.

Aquí un ejemplo:

fn do_something_with_parse_u8(input: felt252) -> Result<u8, felt252> {
    let input_to_u8: u8 = parse_u8(input)?;
    // DO SOMETHING
    let res = input_to_u8 - 1;
    Result::Ok(res)
}

We can see that do_something_with_parse_u8 function takes a felt252 value as input and calls parse_u8 function. The ? operator is used to propagate the error, if any, or unwrap the successful value.

Y con un pequeño caso de prueba:

fn parse_u8(s: felt252) -> Result<u8, felt252> {
    match s.try_into() {
        Option::Some(value) => Result::Ok(value),
        Option::None => Result::Err('Invalid integer'),
    }
}

fn do_something_with_parse_u8(input: felt252) -> Result<u8, felt252> {
    let input_to_u8: u8 = parse_u8(input)?;
    // DO SOMETHING
    let res = input_to_u8 - 1;
    Result::Ok(res)
}

#[cfg(test)]
mod tests {
    use super::*;
    #[test]
    fn test_function_2() {
        let number: felt252 = 258;
        match do_something_with_parse_u8(number) {
            Result::Ok(value) => println!("Result: {}", value),
            Result::Err(e) => println!("Error: {}", e),
        }
    }
}

The console will print the error Invalid Integer.

Resumen

We saw that recoverable errors can be handled in Cairo using the Result enum, which has two variants: Ok and Err. The Result<T, E> enum is generic, with types T and E representing the successful and error values, respectively. The ResultTrait provides methods for working with Result<T, E>, such as unwrapping values, checking if the result is Ok or Err, and panicking with custom messages.

Para gestionar errores recuperables, una función puede devolver un tipo Result y utilizar la concordancia de patrones para gestionar el éxito o el fracaso de una operación. El operador ? puede utilizarse para gestionar errores implícitamente, propagando el error o desenvolviendo el valor correcto. Esto permite una gestión de errores más concisa y clara, en la que el autor de la llamada es responsable de gestionar los errores generados por la función llamada.