Aprende Rust

Lección 16 de 26 Colecciones y errores

Manejo de errores: Result y ?

  • 5 min de lectura
  • 5 ejercicios
  1. Hay dos tipos de errores. Los irrecuperables son bugs: panic! detiene el programa al instante.
  2. Los recuperables se esperan. parse devuelve un Result: Ok(valor) si va bien o Err(error) si falla.
  3. Con match tratas cada caso. Un Err no rompe nada: tú decides qué hacer y el programa sigue.
  4. El operador ? propaga el error: con Ok saca el valor y sigue; con Err, la función termina y lo devuelve.
1/4

Los programas fallan: un fichero que no existe, un texto que no es un número, una conexión que se corta. Muchos lenguajes gestionan estos casos con excepciones que pueden saltar en cualquier momento. Rust no tiene excepciones. En su lugar, distingue dos tipos de errores y te obliga a tratarlos de forma explícita:

  • Irrecuperables: algo que nunca debería pasar, como un bug. El programa se detiene con panic!.
  • Recuperables: algo que puede pasar y que sabes gestionar, como un dato mal escrito. Se representan con el tipo Result<T, E>.

panic!: errores irrecuperables

La macro panic! detiene el programa al instante y muestra tu mensaje junto al fichero y la línea donde ocurrió:

fn main() {
    println!("Antes del panic");
    panic!("¡Algo ha ido muy mal!");
}

No siempre lo provocas tú. Algunas operaciones hacen panic solas cuando reciben algo imposible, como leer una posición que no existe en un vector:

fn main() {
    let numeros = vec![1, 2, 3];
    let n = numeros[99];
    println!("{n}");
}

¿Cuándo usar panic!? Cuando el programa llega a un estado que indica un bug y no tiene sentido seguir: un dato que siempre debería ser válido y no lo es. También es aceptable en prototipos, ejemplos y tests. Para errores que esperas que ocurran (el usuario escribe mal un número, falta un fichero) usa Result.

Result: errores recuperables

Result es un enum de la biblioteca estándar, como Option, con dos variantes:

enum Result<T, E> {
    Ok(T),  // todo ha ido bien: contiene el valor de tipo T
    Err(E), // algo ha fallado: contiene el error de tipo E
}

T y E son tipos genéricos: T es el tipo del valor y E el del error. Los verás a fondo en la próxima lección.

Un ejemplo real: convertir un texto en número con parse. Como el texto puede no ser un número, parse no devuelve un i32, sino un Result<i32, ParseIntError>. Con ::<i32> le indicas a qué tipo quieres convertir:

fn main() {
    for texto in ["42", "hola"] {
        match texto.parse::<i32>() {
            Ok(numero) => println!("{texto} → el doble es {}", numero * 2),
            Err(error) => println!("{texto} → error: {error}"),
        }
    }
}

Igual que con Option, el compilador no te deja usar el número sin comprobar antes qué variante tienes. Es imposible olvidarte del caso de error.

Atajos: unwrap y expect

Escribir un match cada vez es largo. Cuando estás seguro de que no habrá error, o en un prototipo, tienes dos atajos:

  • unwrap(): devuelve el valor de dentro del Ok. Si es Err, hace panic.
  • expect("mensaje"): igual que unwrap, pero el panic muestra tu mensaje, lo que ayuda a encontrar el problema.
fn main() {
    let a: i32 = "10".parse().unwrap();
    println!("a = {a}");

    let b: i32 = "diez".parse().expect("la configuración debe ser un número");
    println!("b = {b}");
}

El primer parse funciona. El segundo hace panic con la configuración debe ser un número: ParseIntError { kind: InvalidDigit }.

Consejo: Prefiere expect a unwrap. El mensaje documenta por qué crees que nunca fallará y, si falla, sabrás exactamente dónde mirar.

Si en caso de error te vale un valor por defecto, usa unwrap_or. Nunca hace panic:

fn main() {
    let edad: u32 = "treinta".parse().unwrap_or(0);
    println!("Edad: {edad}");
}

Transformar el error: map_err

A veces el error que recibes no es el que quieres devolver. map_err transforma el Err y deja el Ok intacto. Recibe una función corta, un closure (|e| ...), que verás a fondo más adelante:

fn parsear_puerto(texto: &str) -> Result<u16, String> {
    texto
        .parse::<u16>()
        .map_err(|e| format!("puerto «{texto}» no válido: {e}"))
}

fn main() {
    println!("{:?}", parsear_puerto("8080"));
    println!("{:?}", parsear_puerto("99999"));
}

El operador ?

Muchas veces una función no sabe qué hacer con un error y lo mejor es propagarlo: devolvérselo a quien la llamó para que decida. Con match queda así:

use std::num::ParseIntError;

fn sumar(a: &str, b: &str) -> Result<i32, ParseIntError> {
    let x = match a.parse::<i32>() {
        Ok(n) => n,
        Err(e) => return Err(e),
    };
    let y = match b.parse::<i32>() {
        Ok(n) => n,
        Err(e) => return Err(e),
    };
    Ok(x + y)
}

Este patrón es tan común que Rust tiene un atajo: el operador ?. Colocado detrás de un Result, si es Ok extrae el valor y la función sigue; si es Err, la función termina ahí y devuelve ese error.

use std::num::ParseIntError;

fn sumar(a: &str, b: &str) -> Result<i32, ParseIntError> {
    let x = a.parse::<i32>()?;
    let y = b.parse::<i32>()?;
    Ok(x + y)
}

fn main() {
    println!("{:?}", sumar("2", "40"));
    println!("{:?}", sumar("2", "cuarenta"));
}

Las dos versiones hacen exactamente lo mismo, pero la segunda se lee de un vistazo. Fíjate en que, al final, el resultado se envuelve en Ok(...).

Solo en funciones que devuelven Result u Option

Como ? puede salir de la función con un error, la función tiene que devolver un Result (o un Option). En un main normal, que no devuelve nada, no compila:

fn main() {
    let n: i32 = "5".parse()?;
    println!("{n}");
}

Con Option funciona igual: si el valor es None, la función devuelve None en ese punto.

fn inicial_mayuscula(texto: &str) -> Option<char> {
    let letra = texto.chars().next()?;
    Some(letra.to_ascii_uppercase())
}

fn main() {
    println!("{:?}", inicial_mayuscula("rust"));
    println!("{:?}", inicial_mayuscula(""));
}

main también puede devolver Result

Para usar ? en main, haz que devuelva un Result. El tipo más cómodo es Result<(), Box<dyn std::error::Error>>, que significa «nada si todo va bien o cualquier tipo de error». ? convierte el error concreto a ese tipo automáticamente:

use std::error::Error;

fn main() -> Result<(), Box<dyn Error>> {
    let ancho: u32 = "8".parse()?;
    let alto: u32 = "5".parse()?;
    println!("Área: {}", ancho * alto);
    Ok(())
}

Si main devuelve un Err, el programa termina mostrando Error: ... y con un código de salida distinto de cero. Verás Box y dyn con calma en las lecciones de traits y smart pointers.

Tus propios errores

En tus programas, lo ideal es describir los errores posibles con un enum. Así quien llame a tu función puede reaccionar a cada caso con match. Para poder imprimirlos con {}, implementa std::fmt::Display:

use std::fmt;

enum ErrorRetirada {
    CantidadCero,
    SaldoInsuficiente { saldo: u32, pedido: u32 },
}

impl fmt::Display for ErrorRetirada {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        match self {
            ErrorRetirada::CantidadCero => write!(f, "no puedes retirar 0 €"),
            ErrorRetirada::SaldoInsuficiente { saldo, pedido } => {
                write!(f, "tienes {saldo} € y quieres retirar {pedido} €")
            }
        }
    }
}

fn retirar(saldo: u32, cantidad: u32) -> Result<u32, ErrorRetirada> {
    if cantidad == 0 {
        return Err(ErrorRetirada::CantidadCero);
    }
    if cantidad > saldo {
        return Err(ErrorRetirada::SaldoInsuficiente { saldo, pedido: cantidad });
    }
    Ok(saldo - cantidad)
}

fn main() {
    for cantidad in [30, 0, 500] {
        match retirar(100, cantidad) {
            Ok(restante) => println!("Retiras {cantidad} €. Te quedan {restante} €"),
            Err(e) => println!("Error: {e}"),
        }
    }
}

write! funciona como format!, pero escribe en el formateador f. Por ahora copia este patrón tal cual: en la lección de traits verás qué significa impl ... for.

Para ir más allá: Si además añades #[derive(Debug)] al enum e impl std::error::Error for ErrorRetirada {}, tu error se podrá propagar con ? desde un main que devuelve Box<dyn Error>.

¿panic o Result?

SituaciónQué usar
Un bug: algo que nunca debería ocurrirpanic!
Prototipos, ejemplos y testsunwrap / expect
Un error esperable que alguien puede gestionarResult con match
Un error con un valor por defecto razonableunwrap_or
No sabes gestionarlo aquí, pero quien te llama sí?

Hora de practicar

Ejercicios

0/5 resueltos

Reto 1 de 5

Recorre entradas y usa un match sobre texto.parse::<i32>(). Si se puede convertir, imprime Número: N; si no, imprime Error: TEXTO no es un número (por ejemplo, Error: hola no es un número).
Salida esperada
Número: 42
Error: hola no es un número
Número: -7
src/main.rs
$ cargo run Listo

Pulsa Ejecutar para compilar y ver aquí la salida de tu programa.

¿Algo no cuadra en este reto? Avísanos