El operador ?: propagar errores con limpieza
El operador ? colapsa el match de propagación en un solo carácter: desenvuelve el Ok o el Some, o hace return temprano del Err o None. La conversión con From, el uso en main, y por qué Option y Result son la base del estilo de Rust.
Ya sabes construir, transformar y desenvolver Option y Result. Falta el gesto más frecuente de todos: cuando una función llama a otra falible y quiere propagar el fallo hacia arriba en lugar de manejarlo aquí. Hacerlo a mano con match es tan repetitivo que ensucia toda función no trivial. El operador ? colapsa ese patrón en un solo carácter, y al hacerlo revela por qué Option y Result no son dos tipos más de la biblioteca, sino la gramática misma sobre la que se escribe Rust.
- Reconocer el
matchde propagación repetitivo que?viene a eliminar. - Usar
?para desenvolver o hacer return temprano sobreResulty sobreOption. - Entender la conversión automática del error con el trait
From, semilla del nivel 23. - Ver por qué
OptionyResultson la base del estilo idiomático de Rust.
Propagar a mano: el ruido del match
Considera una función que lee un fichero, lo recorta y lo devuelve. Cada paso puede fallar, y si falla, queremos devolver ese error al llamador. Escrito a mano:
fn leer_config() -> Result<String, std::io::Error> {
let contenido = match std::fs::read_to_string("config.toml") {
Ok(c) => c,
Err(e) => return Err(e), // propagamos el error tal cual
};
Ok(contenido.trim().to_string())
}
Ese match no aporta ninguna lógica: solo dice “si va bien, sigue con el valor; si va mal, devuélvelo ya”. Es puro ceremonial, y en una función con cinco pasos falibles se multiplica hasta enterrar lo que de verdad hace el código.
El operador ?: la propagación como puntuación
El operador ?, colocado tras una expresión que devuelve Result, hace exactamente eso: si es Ok(v), se evalúa a v y el programa sigue; si es Err(e), ejecuta return Err(e) inmediatamente. La misma función, con ?:
fn leer_config() -> Result<String, std::io::Error> {
let contenido = std::fs::read_to_string("config.toml")?; // desenvuelve o propaga
Ok(contenido.trim().to_string())
}
Una línea, una intención. El ? desaparece visualmente pero mantiene el fallo completamente explícito: está ahí, en el carácter, y el tipo de retorno sigue siendo Result, así que el compilador te obliga a que la función entera sea honesta sobre su falibilidad. No es un try/catch disfrazado: es el cortocircuito de la vía ferroviaria de 7.4 elevado a sintaxis. Encadenar varios pasos se vuelve natural:
fn suma_del_fichero(ruta: &str) -> Result<i32, Box<dyn std::error::Error>> {
let texto = std::fs::read_to_string(ruta)?; // io::Error si falla
let mut total = 0;
for linea in texto.lines() {
total += linea.trim().parse::<i32>()?; // ParseIntError si falla
}
Ok(total)
}
? sobre Option, y la conversión con From
El ? también funciona sobre Option, con la simetría que ya esperas: si es Some(v), se evalúa a v; si es None, hace return None. Eso sí, dentro de una función que devuelva Option (no puedes propagar un None desde una función que devuelve Result, ni al revés):
fn primera_letra_mayus(s: &str) -> Option<char> {
let primera = s.chars().next()?; // si la cadena está vacía, return None
Some(primera.to_ascii_uppercase())
}
Sobre Result hay un poder extra decisivo. Cuando propagas un Err(e) de tipo F desde una función cuyo error es de tipo E, el ? convierte F en E automáticamente llamando a From::from. Por eso el ejemplo anterior funcionaba: io::Error y ParseIntError son distintos, pero ambos se convierten a Box<dyn Error>. Con un tipo de error propio, tú provees las conversiones:
enum MiError {
Io(std::io::Error),
Parse(std::num::ParseIntError),
}
impl From<std::io::Error> for MiError {
fn from(e: std::io::Error) -> Self { MiError::Io(e) }
}
impl From<std::num::ParseIntError> for MiError {
fn from(e: std::num::ParseIntError) -> Self { MiError::Parse(e) }
}
fn cargar(ruta: &str) -> Result<i32, MiError> {
let texto = std::fs::read_to_string(ruta)?; // io::Error → MiError vía From
let n: i32 = texto.trim().parse()?; // ParseIntError → MiError vía From
Ok(n)
}
flowchart TD A[expresion seguida de interrogacion] --> B[Es Ok o Some] A --> C[Es Err o None] B --> D[Se desenvuelve y el programa sigue] C --> E[return temprano con el error convertido por From] E --> F[El llamador recibe el fallo tipado] style B fill:#a6e3a1,color:#11111b style C fill:#f38ba8,color:#11111b style D fill:#89b4fa,color:#11111b style F fill:#cba6f7,color:#11111b
Incluso main puede devolver Result, lo que permite usar ? en el nivel más alto de un programa:
fn main() -> Result<(), Box<dyn std::error::Error>> {
let contenido = std::fs::read_to_string("config.toml")?;
println!("{contenido}");
Ok(()) // si algo falla, main devuelve Err y el proceso sale con código ≠ 0
}
Es tentador leer ? como “ignora el error y sigue”. Es justo lo contrario. ? propaga el error intacto (convertido, pero no perdido) hacia el llamador, que ahora es responsable de él. La honestidad se conserva en toda la cadena: cada función de la pila declara en su firma que puede fallar, y el fallo asciende explícito hasta que alguien decide manejarlo con match o los combinadores de 7.4. ? cambia dónde se maneja el error, nunca si se maneja.
Option y Result como la base del estilo de Rust
Da un paso atrás y mira el nivel entero. Sin excepciones y sin null, Rust necesitaba una forma de expresar “esto podría no dar valor” y “esto podría fallar” que fuera ordinaria: un valor con tipo, visible en las firmas, verificado por el compilador. Esa forma son Option y Result. Y sobre esos dos tipos se levanta todo lo demás: los combinadores de 7.4 los transforman en tuberías, el ? los propaga sin ruido, y el #[must_use] impide olvidarlos. No es una biblioteca de manejo de errores pegada al lenguaje: es el estilo del lenguaje. Cuando leas código Rust idiomático, verás esta gramática por todas partes, y escribirla con fluidez es lo que separa traducir otro lenguaje a Rust de pensar en Rust.
El operador ? parece un truco de comodidad, pero es la clave de bóveda de todo el diseño. Piensa en la tensión que resuelve. Los lenguajes con excepciones logran un camino feliz legible —el código no se llena de comprobaciones— pero al precio de volver el fallo invisible: no aparece en las firmas, es fácil de olvidar, salta por canales ocultos. Los lenguajes que devuelven códigos de error (el C clásico, el Go con su if err != nil) hacen el fallo explícito, pero al precio de sepultar la lógica bajo comprobaciones repetidas en cada línea. Durante décadas pareció que había que elegir: o legibilidad, o honestidad. ? disuelve el dilema. El fallo es un valor —explícito, tipado, imposible de ignorar por descuido gracias a #[must_use]— y a la vez su propagación es un solo carácter que no rompe la lectura del camino feliz. Consigues las dos cosas que parecían incompatibles. Y no es un añadido especial: ? es azúcar sobre match y From, que a su vez operan sobre Option y Result, que a su vez no son más que enum con la optimización de nicho de 6.1. Todo el edificio del manejo de errores de Rust —hasta el thiserror y el anyhow del nivel 23— descansa sobre esta base minúscula y coherente: tipos suma, exhaustividad, valores en vez de excepciones. Dominar este nivel no es aprender dos tipos; es haber entendido la columna vertebral sobre la que se sostiene, en pie, el resto del lenguaje.
- Escribe
leer_configcon elmatchmanual y luego con?; compara línea a línea qué desapareció y qué se conservó. - Escribe una función que devuelva
Option<char>y use?sobrechars().next(); compruébala con una cadena vacía y una con contenido. - Define el
enum MiErrorcon dos variantes y susimpl From, y escribecargarusando?dos veces sobre errores de tipos distintos. - Convierte tu
mainenfn main() -> Result<(), Box<dyn std::error::Error>>y usa?para leer un fichero; borra el fichero y observa el código de salida del proceso. - Intenta usar
?sobre unOptiondentro de una función que devuelveResulty lee el error del compilador; explica por qué los mundos no se mezclan sin una conversión explícita (ok_orde 7.4).