Result a fondo: el fallo como valor y el fin de las excepciones
Result no es una biblioteca de manejo de errores: es un enum ordinario que hace del fallo un valor con tipo, visible en la firma, imposible de ignorar por descuido. Su representación de coste cero en el camino feliz, por qué Rust rechazó las excepciones tras ver fracasar las excepciones comprobadas, y cómo pasar de un String de error a un enum de dominio que implementa el trait Error.
Ya conoces Result<T, E> como el enum de dos variantes que devuelve una función falible. Este nivel lo mira de frente, como decisión de diseño con siglos de historia detrás. La pregunta no es “¿cómo uso Result?”, sino “¿por qué un lenguaje de sistemas moderno renuncia a las excepciones, el mecanismo que dominó la industria durante treinta años, para modelar el fallo como un simple valor de retorno?”. La respuesta toca la representación en memoria, el modelo de coste del optimizador, la componibilidad, y la lección amarga que Java aprendió con las excepciones comprobadas. Entenderla es dejar de traducir otro lenguaje a Rust y empezar a pensar en Rust.
- Ver
Result<T, E>como un tipo suma ordinario y entender su representación de coste cero. - Razonar por qué Rust rechazó las excepciones, comprobadas y no comprobadas.
- Comprender qué garantiza
#[must_use]y por qué el camino feliz no paga desenrollado. - Pasar de un
Eimprovisado (String) a unenumde dominio que implementa el traitError.
El fallo como dato de primera clase
Result no tiene nada de mágico: es un enum genérico como cualquiera que tú escribirías.
enum Result<T, E> {
Ok(T),
Err(E),
}
Que el fallo sea un valor ordinario tiene tres consecuencias inmediatas. Primera: aparece en la firma, así que la falibilidad es parte del contrato público y el compilador la verifica. Segunda: se compone con las mismas herramientas que todo lo demás —match, los combinadores, el operador ?—, sin una gramática aparte. Tercera, y menos obvia: su representación puede ser de coste cero. Cuando uno de los dos lados deja patrones de bits sin usar (un nicho), el compilador aplica la misma optimización que ya viste con Option y representa el Result sin etiqueta extra. Un Result cuyo Ok es una referencia o un NonZero, por ejemplo, ocupa lo mismo que ese valor a secas.
Y sobre el camino feliz no pesa ningún mecanismo de fondo: devolver Ok(v) es devolver un valor por registro o pila, tan barato como devolver v a secas. No hay tablas de desenrollado que consultar, ni salto que preparar. El coste del manejo de errores solo aparece donde hay error, y es el mismo return que el del éxito.
Result lleva el atributo #[must_use]: descartar uno sin inspeccionarlo dispara un aviso del compilador. Es la inversión exacta del olvido fácil de las excepciones no capturadas. Si de verdad quieres tirar el fallo, tienes que decirlo en voz alta con let _ = operacion();, y ese _ deja constancia deliberada en el código. El camino por defecto no es perder el error, sino reconocerlo.
Por qué Rust rechazó las excepciones
En un lenguaje con excepciones, el fallo viaja por un canal invisible. Una función puede lanzar sin que su firma lo diga; el control salta por encima de tu código hasta el catch más cercano, dondequiera que esté. Rust identificó tres costes en ese modelo y no quiso pagar ninguno.
- Invisibilidad. Las excepciones no comprobadas (el modelo de C++, C#, Python) no aparecen en las firmas: no sabes qué puede fallar sin leer toda la implementación y la de cuanto llama. El razonamiento local se rompe.
- El fracaso de las comprobadas. Java intentó lo contrario con las excepciones comprobadas, declaradas en la firma con
throwsy obligatorias de capturar. Sonaba a la solución correcta y se convirtió en una advertencia histórica: eran virales, ensuciaban cada firma, y la presión por silenciarlas empujó a la práctica más nociva posible —capturar y no hacer nada, elcatchvacío— que es peor que no comprobar. - El modelo de coste asimétrico. Las excepciones se venden como “coste cero hasta que se lanzan”; el problema es lo que ocurre cuando se lanzan: desenrollado pesado, y un camino de control que el optimizador no ve, porque puede brotar de casi cualquier llamada. El fallo vive en una dimensión paralela al flujo normal.
Rust elige lo contrario en cada eje: el fallo es un valor ordinario que aparece en el tipo de retorno. No hay canal oculto, no hay salto invisible, no hay segunda gramática para “cuando las cosas van mal”. La fiabilidad deja de depender de la disciplina para recordar los catch y pasa a estar garantizada por el sistema de tipos, sin caer en la trampa de Java, porque el operador ? (siguiente lección) hace la propagación tan barata como un carácter y elimina la fricción que hundió a las comprobadas.
flowchart TD X[Modelo de excepciones] --> X1[El fallo salta por un canal oculto] X1 --> X2[No esta en la firma y es facil de olvidar] Y[Modelo de Result] --> Y1[El fallo es un valor en el tipo de retorno] Y1 --> Y2[Visible en la firma y marcado must_use] Y2 --> Y3[Se compone con match combinadores y el operador ?] style X fill:#f38ba8,color:#11111b style Y fill:#a6e3a1,color:#11111b style Y3 fill:#cba6f7,color:#11111b
Modelar el error: del String al enum de dominio
Elegir bien el tipo E es donde el manejo de errores se vuelve ingeniería. Un Result<T, String> es cómodo para prototipar y pésimo para una API: quien te llama no puede distinguir programáticamente un fallo de otro, solo leer una cadena. La progresión idiomática va del String desechable al enum de dominio, una variante por cada modo de fallo distinguible:
use std::fmt;
#[derive(Debug)]
enum ErrorConfig {
NoEncontrada,
LineaInvalida { numero: usize },
}
impl fmt::Display for ErrorConfig {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self {
ErrorConfig::NoEncontrada => write!(f, "no se encontro la configuracion"),
ErrorConfig::LineaInvalida { numero } => write!(f, "linea invalida: {numero}"),
}
}
}
impl std::error::Error for ErrorConfig {}
Implementar el trait Error —que desde Rust 1.81 vive en core y sirve también en no_std— convierte tu tipo en un ciudadano del ecosistema: encaja en Box<dyn Error>, se integra con las bibliotecas, y su método source permite encadenar la causa subyacente (el error de red que provocó tu error de dominio), formando una cadena de errores que se puede recorrer. Un enum de dominio da a quien te llama el poder de un match exhaustivo sobre los modos de fallo; un Box<dyn Error> borra el tipo concreto cuando solo quieres reportar. La costura que une todos esos tipos heterogéneos —convertir el error de una capa al vocabulario de la siguiente— es el trait From, y es exactamente lo que el operador ? explota en la próxima lección.
El álgebra del fallo: transformar E sin desenvolver
Antes de propagar o manejar, a menudo quieres transformar el Result sin salir de él. Los combinadores son esa álgebra, y para el manejo de errores el central es map_err, que reescribe el error dejando intacto el éxito: es la pieza que adapta el E de una capa al vocabulario de otra sin escribir un match.
fn leer_puerto(cfg: &str) -> Result<u16, ErrorConfig> {
std::fs::read_to_string(cfg)
.map_err(|_| ErrorConfig::NoEncontrada)? // io::Error se reescribe a ErrorConfig
.trim()
.parse()
.map_err(|_| ErrorConfig::LineaInvalida { numero: 0 })
}
map transforma el valor de Ok; map_err, el de Err. and_then encadena otra operación falible sobre el éxito, aplanando el Result anidado que daría map; or_else ofrece una alternativa —también falible— ante el fallo. Juntos dibujan una vía ferroviaria: el valor avanza por el raíl del éxito y, en cuanto surge un Err, salta al raíl del fallo y se salta el resto de transformaciones sin ejecutarlas. El operador ? de la próxima lección es el cortocircuito de esa vía elevado a sintaxis; los combinadores siguen siendo la herramienta cuando quieres transformar el fallo en vez de propagarlo, y map_err en particular es como se adapta un error ajeno al tuyo cuando no quieres un impl From permanente.
La decisión de no tener excepciones parece técnica y es filosófica. Una excepción trata el fallo como algo excepcional: una anomalía que se saca del flujo normal y se manda por un tubo aparte, invisible en las firmas y fácil de olvidar. Rust rechaza la premisa de raíz. En el software real, fallar no es la excepción: es parte del flujo normal. Los ficheros no están, las redes se caen, los usuarios teclean basura, los números se desbordan. Si el fallo es tan ordinario como el éxito, debe viajar por el mismo canal que el éxito —ser un valor, tener un tipo, aparecer en la firma— para que el compilador razone sobre él con las mismas armas que sobre cualquier otro dato. La consecuencia es que el manejo de errores deja de ser un subsistema de control de flujo y se vuelve código normal: componible con match, con los combinadores, con el ?. No hay una segunda gramática que aprender para el fracaso. Y aquí está lo profundo, la lección que Java pagó cara: hacer el fallo explícito no basta, porque si además lo haces incómodo —como las excepciones comprobadas con su ceremonia viral— la gente lo silenciará, y el silencio es peor que la invisibilidad. Rust cierra esa vía por los dos lados a la vez. Por un lado #[must_use] hace que ignorar sea un acto ruidoso y deliberado, no el camino de menor resistencia. Por otro, el operador ? hace que propagar cueste un carácter, de modo que la vía honesta es también la más cómoda. Explícito y ergonómico: esa combinación, que parecía imposible durante décadas de guerra entre códigos de error verbosos y excepciones invisibles, es la que sostiene todo el nivel. Result no es una clase de biblioteca sobre un lenguaje; Result es cómo piensa el lenguaje.
- Escribe una función que devuelva
Result<i32, String>y luego reescríbela con unenumde dominio de dos variantes; argumenta qué gana quien te llama con la segunda versión. - Implementa
Displayystd::error::Errorpara tuenum, y comprueba que encaja en una función que devuelveBox<dyn Error>. - Demuestra la representación de coste cero: compara
size_ofde un tipo con nicho a secas y del mismo envuelto enResultcon unErrde tipo(). - Provoca el aviso de
#[must_use]ignorando unResult, y siléncialo correctamente conlet _ = ...; razona por qué el_explícito es mejor que borrar el aviso. - Escribe en dos o tres frases por qué las excepciones comprobadas de Java “tenían razón en el diagnóstico y se equivocaron en la cura”, y cómo el
?de la siguiente lección evita ese error.