El operador ? en Rust: propagar errores sin escribir match en cada línea

El operador ? es azúcar sintáctico que elimina el boilerplate del manejo de errores en Rust. En lugar de escribir un match en cada llamada que puede fallar, escribes ? al final y el error se propaga automáticamente al llamador. El resultado es código que se lee casi como si no tuviera manejo de errores, pero que es completamente seguro.

Sin ? vs con ?

use std::fs;
use std::io;

// Sin ?
fn leer_usuario_v1(ruta: &str) -> Result<String, io::Error> {
    let contenido = match fs::read_to_string(ruta) {
        Ok(s)  => s,
        Err(e) => return Err(e),
    };
    Ok(contenido.trim().to_string())
}

// Con ?
fn leer_usuario_v2(ruta: &str) -> Result<String, io::Error> {
    let contenido = fs::read_to_string(ruta)?;
    Ok(contenido.trim().to_string())
}

Ambas funciones son equivalentes. ? hace exactamente lo mismo que el match manual: si es Ok, extrae el valor; si es Err, retorna el error.

Cómo funciona internamente

expresión? es equivalente a:

match expresión {
    Ok(val) => val,
    Err(e)  => return Err(From::from(e)),
}

El detalle importante es From::from(e): el operador ? convierte automáticamente el tipo del error usando el trait From. Eso permite encadenar funciones con distintos tipos de error siempre que exista una conversión implementada.

Conversión automática de tipos de error

use std::num::ParseIntError;
use std::io;

#[derive(Debug)]
enum MiError {
    Io(io::Error),
    Parse(ParseIntError),
}

impl From<io::Error> for MiError {
    fn from(e: io::Error) -> Self { MiError::Io(e) }
}

impl From<ParseIntError> for MiError {
    fn from(e: ParseIntError) -> Self { MiError::Parse(e) }
}

fn leer_numero(ruta: &str) -> Result<i32, MiError> {
    let contenido = std::fs::read_to_string(ruta)?; // io::Error ? MiError::Io
    let numero: i32 = contenido.trim().parse()?;    // ParseIntError ? MiError::Parse
    Ok(numero)
}

Encadenar con ?

fn obtener_config(ruta: &str) -> Result<Config, Box<dyn std::error::Error>> {
    let contenido = std::fs::read_to_string(ruta)?;
    let config: Config = serde_json::from_str(&contenido)?;
    Ok(config)
}

Cuatro operaciones que pueden fallar, cero match. El código se lee casi como si fuera secuencial sin errores.

? en main()

Desde Rust 1.26, main() puede devolver Result:

use std::fs;

fn main() -> Result<(), Box<dyn std::error::Error>> {
    let contenido = fs::read_to_string("config.txt")?;
    println!("{}", contenido);
    Ok(())
}

Si el programa retorna Err, Rust imprime el error y sale con código 1.

? con Option

El operador ? también funciona con Option: retorna None si el valor es None.

fn primera_linea(texto: &str) -> Option<&str> {
    texto.lines().next()  // Option, no Result
}

fn primera_palabra_primera_linea(texto: &str) -> Option<&str> {
    let linea = primera_linea(texto)?; // retorna None si no hay líneas
    linea.split_whitespace().next()    // retorna None si la línea está vacía
}

Limitación: no mezcles ? de Result y Option

// ERROR: no puedes usar ? de Option en una función que retorna Result
fn mezcla(ruta: &str) -> Result<i32, io::Error> {
    let contenido = std::fs::read_to_string(ruta)?; // OK
    let num = contenido.trim().parse::<i32>().ok()?; // ERROR: ? de Option en Result
    Ok(num)
}

// Solución: convierte manualmente
fn mezcla_ok(ruta: &str) -> Result<i32, io::Error> {
    let contenido = std::fs::read_to_string(ruta)?;
    contenido.trim().parse().map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))
}

Resumen

  • ? extrae el valor de Ok o retorna el Err al llamador.
  • Convierte automáticamente el tipo del error usando el trait From.
  • Se puede usar en funciones que retornan Result o Option.
  • Desde Rust 1.26, también en main().
  • No mezcles ? de Result y de Option en la misma función sin conversión explícita.

El siguiente artículo cubre panic!: cuándo es apropiado dejar que el programa falle de forma inmediata y cuándo es mejor devolver un Result.

COMPARTE ESTE ARTÍCULO

COMPARTIR EN FACEBOOK
COMPARTIR EN TWITTER
COMPARTIR EN LINKEDIN
COMPARTIR EN WHATSAPP