El operador ? es un “azúcar sintáctico” diseñado para simplificar el manejo de errores, evitando la necesidad de escribir repetitivos bloques match para cada operación que pueda fallar. Su función principal es la propagación: si una expresión devuelve un error, el operador detiene la ejecución de la función actual y devuelve ese error inmediatamente al llamador.

Este operador funciona de manera distinta según el tipo de retorno de la expresión:
1. En tipos Result<T, E>: Si el resultado es Ok(v), el operador extrae el valor v y permite que la ejecución continúe. Si es Err(e), la función actual retorna prematuramente Err(e.into()). Este into() es crucial, ya que utiliza la convención From para convertir automáticamente el tipo de error actual (e) al tipo de error definido en la firma de la función receptora.
2. En tipos Option<T>: Si el valor es Some(v), se extrae v. Si es None, la función retorna None de inmediato.

El uso de ? es extremadamente potente para el encadenamiento de operaciones. Por ejemplo, se puede leer un archivo, procesar su contenido y convertirlo a un número en una sola línea de flujo lógico, donde cada paso que falle interrumpirá la cadena y propagará el error correspondiente. Sin embargo, existe una restricción fundamental: el operador ? solo puede utilizarse en funciones cuyo tipo de retorno sea compatible con Result o Option. Intentar usarlo en una función que devuelve un tipo simple (como i32 o String) resultará en un error de compilación.

use std::io::{self, ErrorKind};

/// Función que simula el procesamiento de una cadena de texto.
/// Retorna un Result que puede contener un error genérico (Box<dyn Error>).
fn procesar_entrada(entrada: &str) -> Result<i32, Box<dyn std::error::Error>> {
    // Simulamos una validación que puede fallar (como un archivo vacío)
    // Si la entrada está vacía, retornamos un error de I/O manualmente.
    if entrada.is_empty() {
        return Err(Box::new(io::Error::new(ErrorKind::InvalidData, "Entrada está vacía")));
    }

    // El operador ? extrae el valor si es Ok, o retorna el error si es Err.
    // Aquí, parse() devuelve ParseIntError, pero el '?' lo convierte 
    // automáticamente a Box<dyn Error> gracias a la implementación de From.
    let numero: i32 = entrada.trim().parse()?;

    // Si todo sale bien, envolvemos el resultado en un Ok.
    Ok(numero)
}

fn main() -> Result<(), Box<dyn std::error::Error>> {
    let datos_validos = "  42  ";
    let datos_invalidos = "no_es_un_numero";

    // Caso 1: Uso exitoso. El valor se extrae y se imprime.
    let n1 = procesar_entrada(datos_validos)?;
    println!("Número procesado con éxito: {}", n1);

    // Caso 2: Propagación de error.
    // El operador '?' en main permite que el error de procesar_entrada
    // se propague hacia el entorno de ejecución, cerrando el programa con error.
    println!("Intentando procesar dato inválido...");
    let _n2 = procesar_entrada(datos_invalidos)?;

    Ok(())
}

Análisis del Código

  • fn procesar_entrada(entrada: &str) -> Result<i32, Box<dyn std::error::Error>>: La firma de la función indica que puede devolver un entero o cualquier tipo de error que implemente el trait Error mediante el uso de un puntero de función dinámico (Box<dyn Error>). Esto es vital para que el operador ? pueda convertir diferentes tipos de errores en uno solo.
  • if entrada.is_empty() { return Err(...) }: Aquí se realiza una validación manual. Si se cumple la condición, se retorna un error de tipo std::io::Error.
  • let numero: i32 = entrada.trim().parse()?;: Esta es la línea clave. El método parse() devuelve un Result<i32, ParseIntError>. El operador ? detecta que el tipo de retorno de procesar_entrada es Box<dyn Error> y llama automáticamente a From::from(error_de_parse) para convertir el ParseIntError en un Box<dyn Error>. Si la operación fuera exitosa, numero contendría el valor i32 extraído.
  • fn main() -> Result<(), Box<dyn std::error::Error>>: Es una práctica común en aplicaciones Rust para permitir el uso de ? desde el punto de entrada principal. Si main retorna un Err, Rust imprimirá el error de forma descriptiva y finalizará con un código de salida de error.
  • let n1 = procesar_entrada(datos_validos)?;: En el flujo normal, el ? extrae el valor 42 y lo asigna a n1.
  • let _n2 = procesar_entrada(datos_invalidos)?;: Al llamar a procesar_entrada con un texto no numérico, parse() devuelve un Err. El operador ? intercepta este error y lo devuelve inmediatamente desde main, evitando que la línea de println! siguiente se ejecute.

59