=> Volver al inicio

Manejo de excepciones en Rust

Publicado el: 2019-12-20

Uno de los temas que mas me ha costado aprender en Rust ha sido la manera de manejar los posibles errores que se puedan tener en tiempo de ejecución ya que, a diferencia de los demás lenguajes que ya había manejado (Python, Ruby o Java) ya que el concepto de "excepción" no existe como tal, sino que todo se trabaja mediante "panic!", "Option" y "Result" y unos métodos unwrap o unwrap_or y en este post explicaré cada uno de ellos.

panic!

Es imposible de detener porque a nivel de ensamblador se está mandando llamar a la instrucción `ud2` la cual genera un código de ejecución inválido y eso detiene en seco el hilo principal.

// Rust

fn main() {

panic!();

}

; Ensamblador

...

example::main:

push    rax

lea     rdi, [rip + .L__unnamed_9]

lea     rdx, [rip + .L__unnamed_10]

mov     rax, qword ptr [rip + std::panicking::begin_panic@GOTPCREL]

mov     esi, 14

call    rax

ud2

^ ^

╚═╩═ Generación de un código inválido ejecución

...

Su funcionamiento es de lo mas sencillo y tiene un cierto parecido a la función `sys.exit` de Python, solo que el macro `panic!` acepta un `str` como parámetro para indicarle al usuario cual fue el posible error y este es opcional.

// Código

fn main() {

panic!();

}

// Salida

...

thread 'main' panicked at 'explicit panic', src/main.rs:2:5

note: run with RUST_BACKTRACE=1 environment variable to display a backtrace.

// Código

fn main() {

panic!("Error en la función 'main'");

}

// Salida

...

thread 'main' panicked at 'Error en la función 'main'', src/main.rs:2:5

note: run with RUST_BACKTRACE=1 environment variable to display a backtrace.

## Option

Ahora si comenzamos con el manejo de excepciones como tal.

Option es un `Enum` (Si no estás familiarizado con el término "Enum" pero si con el término "Tipo", en tu mente reemplaza uno por otro, para este caso son parecidos) que puede contener 2 posibles valores mutuamente excluyentes, y son "algún valor" o "ninguno", que traducido a lenguaje Rust sería un `Some` o un `None`.

Este `Enum` se tendrá que usar cuando nuestra función es posible que devuelva un valor o que no devuelva nada.

La declaración en el campo a retornar es muy simple, solo se debe indicar que la función retornará una instancia del Enum `Option`, un diamante (`<>`) y dentro del diamante el tipo de datos del posible valor a retornar.

El valor a retornar debe estar "encapsulado" dentro de otro Enum llamado `Some` y el dato dentro de ella debe ser del mismo tipo que la declarada dentro del diamante en la cabecera de la función.

fn factorial(n: i32) -> Option {

                    ^ ^ ^  ^ ^

                    ║ ║ ║  ╚═╩═ Tipo de dato del posible valor a retornar

                    ╚═╩═╩═ Nombre del Enum

if n < 0 {

    None

    ^ ^

    ╚═╩═ Se retorna el Enum "None" cuando no se retorna un valor

} else if n == 0 {

    Some(1)

    ^ ^  ^

    ║ ║  ╚═ Valor que se retornará

    ╚═╩═ Enum Some

} else {

    let mut total = 1;

    for value in 1..n {

        total *= value;

    }

    Some(total)

    ^ ^  ^ ^ ^

    ║ ║  ╚═╩═╩═ Valor que se retornará

    ╚═╩═ Enum Some

}

}

Como se puede ver en el ejemplo, la función `factorial` puede recibir un parámetro del tipo `i32` y retornará un posible valor cuyo tipo de dato será también `i32` y dentro de la función se retornan el Enum `None` en caso de que no se deseé retornar ningún valor, o un Enum `Some` cuando la función deba retornar un valor. simple, eh? Ahora vamos a ver como lidiar con lo que retorne esa función.

Una de las maneras mas sencillas de trabajar con los Option es con la estructura de control `match`, poniendo las posibles opciones como criterios de coincidencia dentro.

Aquí un ejemplo:

fn factorial(n: i32) -> Option { ... }

fn main() {

let valor = factorial(10);

            ^ ^ ^ ^ ^ ^ ^

            ╚═╩═╩═╩═╩═╩═╩═ Llamada a la función "factorial"

match valor {

    Some(1) => println!("Posible factorial de 0 o 1"),

    ^ ^  ^

    ║ ║  ╚═ Criterio de coincidencia exacto

    ╚═╩═ Enum Some

    Some(n) => println!("El factorial es: {}", n),

    ^ ^  ^

    ║ ║  ╚═ Criterio de coincidencia con un identificador

    ║ ║     Se puede acceder a el desde el bloque de ejecución

    ╚═╩═ Enum Some

    None => println!("No se puede sacar el factorial de un número negativo"),

    ^ ^

    ╚═╩═ Enum None

}

}

Como se puede ver en el ejemplo, utilizando `match` se pueden declarar los posibles criterios de coincidencia, ya sea con valores (`Some(1)`) para que solo se ejecute el bloque si la función retorna explícitamente un `Some` con valor "1". O también con una variable `Some(n)` y esa variable se puede utilizar dentro del bloque a ejecutar y su Scope no va mas allá. Y por último también se puede hacer la coincidencia con el Enum `None` cuando la función no deba retornar nada.

Como podemos ver, la utilización del Enum `Option` es una manera mas elegante de hacer cosas _hacky_ como devolver un "-1" cuando una función puede devolver un valor numérico.

## Result

Los valores a retornar deberán ser "encapsulados" en el Enum Ok y en el Enum Err y debe corresponder al tipo de dato correspondiente al colocado dentro del diamante.

Como por ejemplo:

fn div(num1: i32, num2: i32) -> Result {
                                ^ ^ ^  ^ ^  ^ ^ ^ ^ ^ ^
                                ║ ║ ║  ║ ║  ╚═╩═╩═╩═╩═╩═ Tipo de dato del error
                                ║ ║ ║  ╚═╩═ Tipo de datos para el caso de éxito
                                ╚═╩═╩═ Nombre del Enum
    if num2 == 0 {
        Err("No se puede dividir un número entre 0")
        ^ ^  ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^
        ║ ║  ╚═╩═╩═╩═╩═╩═╩═╩═╩═╩═╩═╩═╩═╩═╩═╩═╩═╩═╩═ Valor que retornará como error
        ╚═╩═ Nombre del Enum
    } else {
        Ok(num1 as f64 / num2 as f64)
        ^  ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^
        ║  ╚═╩═╩═╩═╩═╩═╩═╩═╩═╩═╩═╩═╩═ Valor que retornará en caso de éxito
        ╚═ Nombre del Enum
    }
}

Como se puede ver en el ejemplo, en la cabecera de la función, en la parte donde se especifica que va a retornar, se debe colocar el Enum Result y dentro de un diamante los 2 tipos de datos, en este caso será un f64 para los casos de éxito y un &'static str (str estático) para los casos erróneos y dentro de la propia función se "encapsulan" los posibles valores de retorno en los Enum Ok y Err para los casos de éxito y de error según correspondan.

Ahora, la manera de lidiar con el valor de una función que retorna un Result es muy similar a la manera de tratar con los Enum Option... con un match` y dentro, todas los posibles criterios de coincidencia.

fn div(num1: i32, num2: i32) -> Result { ... }

fn main() {
    let valor = div(10, 2);
                ^ ^ ^ ^ ^
                ╚═╩═╩═╩═╩═ Llamada a la función "div"
    match valor {
        Ok(3.14159) => println!("wow, tu división retorna el número pi"),
        ^  ^ ^ ^ ^
        ║  ╚═╩═╩═╩═ Criterio de coincidencia exacto
        ╚═ Enum Ok
        Ok(n) => println!("El resultado es: {}", n),
        ^  ^
        ║  ╚═ Criterio de coincidencia con un identificador
        ║     Se puede acceder a el desde el bloque de ejecución
        ╚═ Enum Ok
        Err(e) => println!("{}", e),
        ^ ^ ^
        ║ ║ ╚═ Criterio de coincidencia con un identificador
        ║ ║    Se puede acceder a el desde el bloque de ejecución
        ╚═╩═ Enum Err
    }
}

Se manda a comparar el valor retornado por la función utilizando un match y los criterios deben ser los Enum Ok con un valor exacto, con una variable que se puede utilizar dentro del bloque de ejecución o el Enum Err que sigue las mismas reglas, se puede poner un valor exacto o una variable para utilizar dentro del bloque de ejecución.

pro-tip 1:

Si por algún motivo no te interesa el valor retornado dentro del Ok, Err o Some puede poner como identificador un "_", así el compilador ignorará ese valor por completo y será un criterio de coincidencia válido, como por ejemplo:

fn div(num1: i32, num2: i32) -> Result { ... }

fn main() {
    match div(10, 0) {
        Ok(_) => println!("La división se ejecutó correctamente"),
        ^  ^
        ║  ╚═ Criterio de coincidencia con "_"
        ║     El identificador al ser un "_", el valor nunca es guardado en memoria y es inaccesible
        ╚═ Enum Ok
        Err(_) => println!("Error ejecutando la división"),
        ^ ^ ^
        ║ ║ ╚═ Criterio de coincidencia con "_"
        ║ ║    El identificador al ser un "_", el valor nunca es guardado en memoria y es inaccesible
        ╚═╩═ Enum Err
    }
}

pro-tip 2:

Para mantener mas elegante nuestro código, es recomendable no retornar str's en los mensajes de error de los Result, es mucho mejor implementar Enum's propios para cada posible error.

enum MathError {
    ZeroDivision,
    Domain
}

fn div(num1: i32, num2: i32) -> Result {
                                            ^ ^ ^ ^ ^
                                            ╚═╩═╩═╩═╩═ Nombre de la estructura que contiene los posibles errores
    if num2 == 0 {
        Err(MathError::ZeroDivision)
            ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^
            ╚═╩═╩═╩═╩═╩═╩═╩═╩═╩═╩═╩═ Valor dentro del Enum que contiene nuestro error deseado
    } else {
        Ok(num1 as f64 / num2 as f64)
    }
}

fn main() {
    match div(10, 0) {
        Ok(n) => println!("El resultado es: {}", n),
        Err(MathError::ZeroDivision) => println!("Oh no!, División entre 0"),
            ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^
            ╚═╩═╩═╩═╩═╩═╩═╩═╩═╩═╩═╩═ Criterio de coincidencia más explícito para los errores
        _ => println!("Error desconocido"),
        ^
        ╚═ Como nuestro Enum de errores tiene mas de un valor aparte del "ZeroDivision",
           se debe declarar una opción por defecto como criterio de coincidencia 
    }
}

Los métodos unwrap

Los match son maneras muy elegantes para manejar los Option o los Result, pero en algunas ocasiones quizás querremos lidiar con ellos de una manera menos verbosa y justo para ese motivo existe la gama de métodos unwrap.

El comportamiento de los métodos unwarp dependerá si el Option o el Result tiene un resultado exitoso y son los siguientes:

+-------------------+-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+------------------------------------------------------------------------------------------------------+
| Nombre del método | Comportamiento                                                                                                                                                                                                                                                                                                                                                                                  | Ejemplo                                                                                              |
+============================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================+
| unwrap            | Intenta obtener el valor encapsulado dentro de un Some o un Ok. Sí la función no devolvió alguno de ellos y se ejecutará el macro panic!. No recibe parámetros.                                                                                                                                                                                                                                 | let valor = factorial(1).unwrap(0);                                                                  |
|-------------------+-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+------------------------------------------------------------------------------------------------------|
| unwrap_or         | Intenta obtener el valor encapsulado dentro de un Some o un  Ok. Si la función devolvió un None o un Err, la función devolverá el valor pasado como parámetro que deberá ser del mismo tipo de dato declarado en el diamante para el caso de éxito. Recibe como parámetro el valor a devolver.                                                                                                  | let valor = div(10, 0).unwrap_or(0f64);                                                              |
|-------------------+-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+------------------------------------------------------------------------------------------------------|
| unwrap_or_else    | Intenta obtener el valor encapsulado dentro de un Some o un Ok. En caso de que la función haya devuelto un None o un Err la función retornará el valor que devuelva el closure pasado como parámetro. Recibe como parámetro el closue a ejecutar, en caso de que la función retorne un Result, el closure deberá tener un argumento y si retorna un Option el closure no debe tener argumentos. | let valor = factorial(-1).unwrap_or_else(\|\| 0); let valor = div(10, 0).unwrap_or_else(\|_\| 0f64); |
|-------------------+-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+------------------------------------------------------------------------------------------------------|
| unwrap_or_default | Intentará obtener el valor encapsulado dentro de un Some o un Ok. Si el valor fue ninguno o un error, el método retornará los valores por defecto de cada tipo de dato. No recibe ningún parámetro.                                                                                                                                                                                             | let valor = factorial(0).unwrap_or_default();                                                        |
|-------------------+-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+------------------------------------------------------------------------------------------------------|
| expect            | Intenta obtener el valor encapsulado dentro de un Some o un Ok. Si la función retornó un None o un Err, el método mandará a llamar al macro panic! con el mensaje que se le pase como parámetro. Recibe un parámetro del tipo "str"                                                                                                                                                             | let valor = div(0, 0).expect("Oh no");                                                               |
+-------------------+-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+------------------------------------------------------------------------------------------------------+

Espero que les haya gustado y que les haya servido la información aquí proporcionada. Pueden comentarme mediante mi twitter @kirbylife si algo de lo aquí leído es incorrecto o confuso. Con gusto los leeré :).

=> @kirbylife

Fuentes

=> https://doc.rust-lang.org/std/result/enum.Result.html | https://doc.rust-lang.org/std/option/enum.Option.html | https://doc.rust-lang.org/book/ | https://rust.godbolt.org | https://learning-rust.github.io/docs/e3.option_and_result.html

Actualizaciones

23-12-2019: Arreglados algunos typos. Gracias Nacho.

28-02-2020: Arreglados algunos typos. Gracias VMS.

17-06-2020: Arreglado un typo. Gracias Marc.

=> Marc

Proxy Information
Original URL
gemini://blog.kirbylife.dev/post/manejo-de-excepciones-en-rust-3
Status Code
Success (20)
Meta
text/gemini; charset=utf-8; lang=en
Capsule Response Time
714.928154 milliseconds
Gemini-to-HTML Time
4.339606 milliseconds

This content has been proxied by September (ba2dc).