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 deOko retorna elErral llamador.- Convierte automáticamente el tipo del error usando el trait
From. - Se puede usar en funciones que retornan
ResultoOption. - Desde Rust 1.26, también en
main(). - No mezcles
?deResulty deOptionen 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.
