From, ? y la estrategia: tipado en librerías, opaco en binarios
El operador ? convierte errores llamando a From: un único mecanismo que thiserror y anyhow dirigen a destinos opuestos. Una capa librería estrecha hacia un enum tipado; una capa binario ensancha hacia anyhow con contexto. La síntesis de todo el nivel.
Todas las piezas del nivel encajan en una sola frase: ? convierte el error que propaga llamando a From. Ese hecho, que sembraste al estudiar las conversiones, es el mecanismo común bajo thiserror y anyhow. La macro de thiserror genera los impl From que hacen que ? estreche los errores primitivos hacia tu enum tipado; anyhow aporta una conversión general que hace que ? ensanche cualquier error hacia su tipo universal. Mismo operador, mismo trait, filosofías opuestas. Verlo así disuelve el falso dilema “thiserror o anyhow”: no son rivales, son los dos extremos naturales de una misma costura de conversión, y la pregunta de diseño no es cuál usar sino en qué capa estás.
- Recordar que
?desazucara a unmatchconFrom::fromsobre el error. - Ver que
#[from]dethiserrory la conversión general deanyhowson dos usos del mismo mecanismo. - Componer una capa librería tipada con una capa binario opaca a través de dos
?. - Formular la estrategia:
thiserrorpara librerías,anyhowpara binarios, y por qué componen.
? convierte con From: el mecanismo común
Recupera el desazucarado del operador ? sobre un error: cuando la operación falla, no devuelve el error tal cual, sino que lo pasa por From::from para adaptarlo al tipo de error de la función que lo contiene.
// v? sobre un Result que falla equivale, grosso modo, a:
match v {
Ok(x) => x,
Err(e) => return Err(From::from(e)), // From adapta el error al tipo de la funcion
}
Esto significa que cada ? es una conversión de tipo en potencia, gobernada por From. Y aquí está la revelación del nivel: las dos crates que has visto no hacen otra cosa que proveer el From adecuado para que ese ? apunte a donde ellas quieren.
thiserror, con#[from], genera unimpl From<ErrorPrimitivo> for TuEnum. El?entonces estrecha: unio::Errorse convierte en una variante concreta de tu tipo cerrado.anyhowprovee una implementación generalimpl<E: Error + Send + Sync + 'static> From<E> for anyhow::Error. El?entonces ensancha: cualquier error se convierte en el único tipo universal.
Un operador, un trait, dos direcciones: hacia un tipo específico o hacia un tipo que lo absorbe todo.
thiserror y anyhow comparten la misma costura
La figura clave de todo el nivel es ver ambas capas cooperando. Abajo, una librería con error tipado; arriba, un binario que lo absorbe con contexto.
// === CAPA LIBRERIA: error tipado con thiserror ===
use thiserror::Error;
#[derive(Debug, Error)]
pub enum ErrorRepo {
#[error("registro {0} no encontrado")]
NoEncontrado(u64),
#[error(transparent)]
Io(#[from] std::io::Error),
}
pub fn leer_registro(id: u64) -> Result<String, ErrorRepo> {
let ruta = format!("db/{id}.rec");
let datos = std::fs::read_to_string(&ruta)?; // io::Error -> ErrorRepo por #[from]
if datos.trim().is_empty() {
return Err(ErrorRepo::NoEncontrado(id));
}
Ok(datos)
}
// === CAPA BINARIO: error opaco con anyhow ===
use anyhow::{Context, Result};
fn main() -> Result<()> {
let reg = leer_registro(42)
.context("cargando el registro del usuario activo")?; // ErrorRepo -> anyhow::Error
println!("registro: {reg}");
Ok(())
}
Sigue los dos ? con el dedo, porque cuentan toda la historia:
- Dentro de la librería,
?convierte unio::ErrorenErrorRepo: un estrechamiento hacia el vocabulario cerrado de la librería, y elimpl Fromque lo hace posible lo generó#[from]. Quien llame aleer_registrorecibe unErrorRepotipado y puede hacermatchentreNoEncontradoeIo. - Dentro del binario,
?convierte eseErrorRepoenanyhow::Error: un ensanchamiento hacia el tipo universal, más una frase de contexto.mainno hacematch; solo reporta.
Para que el segundo ? funcione, ErrorRepo ha de ser Send + Sync + 'static. Y lo es automáticamente, porque un error de thiserror es un enum de datos en propiedad y sus campos (u64, std::io::Error) ya lo son. Ese es el contrato silencioso que permite que las dos crates encajen sin que muevas un dedo.
La frontera: dónde tipar y dónde volverse opaco
La estrategia, dicha sin rodeos, es una pregunta sobre tu consumidor aguas abajo:
thiserror en librerías
Tu error es parte de la API pública. El llamador necesita hacer match, manejar, recuperarse. Dale un enum tipado, exhaustivo y estable. Conversión que estrecha hacia un vocabulario cerrado.
anyhow en binarios
Tu error es fontanería interna. Nadie aguas abajo hace match: propaga con ?, anota con context, reporta en main. Conversión que ensancha hacia un tipo universal.
Tres matices afinan la regla:
- El criterio es la superficie pública, no si el crate es binario o librería. Un binario puede usar
thiserroren un módulo interno cuya lógica sí quiere ramificar por tipo de fallo. - Puedes cruzar de vuelta cuando, excepcionalmente, una aplicación necesita decidir:
anyhow::Errorrecupera el tipo concreto condowncast_ref. - Nunca al revés:
anyhow::Erroren la firma pública de una librería roba a tus dependientes el tipado y les impone la crate.
flowchart LR A[io Error del sistema] -->|interrogacion con from de thiserror| B[ErrorRepo tipado y matchable] B -->|interrogacion con context de anyhow| C[anyhow Error opaco con contexto] C --> D[main solo informa y sale] style A fill:#f38ba8,color:#11111b style B fill:#a6e3a1,color:#11111b style C fill:#cba6f7,color:#11111b style D fill:#89b4fa,color:#11111b
El nivel entero converge aquí, y la síntesis es más simple de lo que parecía. From con ? es un mecanismo único y uniforme de conversión, y el arte completo del diseño de errores en Rust se reduce a elegir el tipo destino de esa conversión en cada capa. Piensa en un programa como una sucesión de vocabularios anidados. En el fondo, los errores primitivos del sistema operativo y de los parsers. Al ascender hacia una librería, los traduces a un vocabulario curado y cerrado —un enum tipado— porque tus consumidores son código que debe razonar sobre el fallo para decidir. Al ascender más allá de la frontera de la librería, hacia la aplicación, dejas de traducir y empiezas a acumular: ya no te interesa nombrar cada fallo, solo anotarlo con dónde-estabas y dejarlo subir hasta un lugar donde se registra y se sale. thiserror sirve al primer movimiento —traducción hacia un conjunto cerrado— y anyhow al segundo —acumulación hacia uno abierto—, pero ambos son el mismo ? llamando al mismo From; la única diferencia es el tipo a la izquierda de la asignación. Esa es la unidad profunda: Rust no te dio dos sistemas de manejo de errores. Te dio un operador de conversión y un trait de conversión, y dos crates que los apuntan a clases opuestas de destino, una estrechando y otra ensanchando. Cuando ves que thiserror y anyhow no compiten sino que son los dos extremos naturales de la conversión dirigida por From, dejas de preguntar “¿qué crate?” y empiezas a hacer la pregunta verdadera: en esta capa, ¿mi consumidor necesita decidir según el error, o solo saber que ocurrió? La respuesta elige la crate por ti, siempre.
? convierte el error con From::from. thiserror genera esos From para estrechar hacia tu enum tipado; anyhow aporta un From general para ensanchar hacia su tipo universal. Compón las capas: la librería devuelve un error tipado (matchable), y el binario lo absorbe con ? en anyhow::Error añadiendo context. Funciona porque los errores de thiserror son Send + Sync + 'static. La regla: tipa donde tu consumidor razone (librerías), vuélvete opaco donde solo se reporte (binarios).
- Escribe un
enum ErrorRepoconthiserror(una variante propia y una con#[from] std::io::Error) y una función de librería que devuelvaResult<_, ErrorRepo>usando?. Identifica dónde inserta el compilador la conversión conFrom. - Desde un
mainconanyhow::Result, llama a esa función con?ycontext. Traza qué conversión hace cada uno de los dos?. - Elimina el
derive(Debug)o algún campo para romper la cotaSend + Sync + 'staticdeErrorRepoy observa cómoanyhowdeja de aceptarlo en el segundo?. - En el
main, usadowncast_refsobre elanyhow::Errorpara recuperar elErrorRepoconcreto y ramificar. Argumenta por qué esto debe ser la excepción y no la norma. - Explica, con la metáfora de los vocabularios anidados, por qué poner
anyhow::Erroren la firma pública de la librería sería un error de diseño, y qué le arrebataría a sus consumidores.