anyhow: un solo tipo de error, con contexto, para aplicaciones
En un binario nadie aguas abajo hace match sobre tus errores: solo quieres propagar y reportar. anyhow ofrece un único tipo que absorbe cualquier error, un método context para narrar el fallo y macros como bail para abortar. Ergonomía y diagnóstico a cambio del tipo.
En una aplicación la pregunta cambia. Nadie va a construir lógica sobre tus errores internos; al final del día un fallo sube hasta main, se registra y el proceso termina con código distinto de cero. Enumerar cuarenta modos de fallo con un enum sería trabajo desperdiciado: no vas a escribir recuperación específica para cada uno. Lo que quieres es propagar sin fricción, añadir contexto por el camino —“esto ocurrió mientras cargaba la configuración”— y obtener al final un informe legible. Esa es exactamente la propuesta de anyhow: un único tipo de error que traga cualquier cosa, contexto de una línea y macros para abortar. Renuncia a que puedas hacer match, pero en un binario esa renuncia no cuesta nada.
- Usar
anyhow::Result<T>y el tipoanyhow::Errorque absorbe cualquier error vía?. - Enriquecer un fallo con
contextywith_contextconstruyendo la cadena de causas. - Abortar con las macros
bail!yensure!, y fabricar errores al vuelo conanyhow!. - Reportar la cadena completa devolviendo
anyhow::Resultdesdemain.
anyhow::Result y el error que lo traga todo
Se declara la dependencia y se cambia el tipo de retorno:
[dependencies]
anyhow = "1"
use anyhow::{Context, Result};
fn leer_umbral(ruta: &str) -> Result<u32> {
let texto = std::fs::read_to_string(ruta)
.with_context(|| format!("no se pudo leer '{ruta}'"))?;
let umbral: u32 = texto.trim().parse()
.with_context(|| format!("'{}' no es un numero valido", texto.trim()))?;
Ok(umbral)
}
anyhow::Result<T> no es más que un alias de Result<T, anyhow::Error>: un solo tipo de error para toda la aplicación. Y anyhow::Error es un puntero inteligente del tamaño de una palabra que envuelve cualquier tipo que sea Error + Send + Sync + 'static, capturando además un backtrace. Su implementación general de From es la que hace que los dos ? de arriba funcionen sobre errores de tipos distintos —un io::Error y un ParseIntError— sin que escribas ni una conversión. Donde thiserror te obligaba a nombrar cada fallo, anyhow te libera de nombrar ninguno.
context: la narrativa del fallo
El valor real de anyhow no es tragar errores, sino contarlos. Los métodos context y with_context envuelven un error añadiendo una frase que explica qué estabas intentando, y esa frase se convierte en un nuevo eslabón de la cadena de source que viste en la primera lección:
use anyhow::{Context, Result};
fn arrancar() -> Result<()> {
let umbral = leer_umbral("umbral.txt")
.context("configurando el limitador de peticiones")?;
println!("umbral: {umbral}");
Ok(())
}
La distinción entre las dos formas importa: context(msg) toma un mensaje ya formado; with_context(|| ...) toma una clausura que solo se ejecuta si hay error, ideal cuando construir el mensaje cuesta una asignación o un format!. Usa with_context siempre que el mensaje lleve interpolación, para no pagar el formateo en el camino feliz.
bail, ensure y anyhow: errores al vuelo
No todos los fallos vienen de otra función; muchos los detectas tú con una condición. anyhow da tres atajos para eso:
use anyhow::{anyhow, bail, ensure, Result};
use std::collections::HashMap;
fn validar(edad: i32) -> Result<()> {
if edad < 0 {
bail!("la edad no puede ser negativa: {edad}"); // return Err(anyhow!(...))
}
ensure!(edad <= 150, "edad implausible: {edad}"); // if !cond { bail!(...) }
Ok(())
}
fn buscar(tabla: &HashMap<u32, String>, id: u32) -> Result<String> {
tabla.get(&id)
.cloned()
.ok_or_else(|| anyhow!("no existe el usuario {id}")) // Option -> Result
}
anyhow!("...")fabrica un error ad-hoc a partir de un mensaje, sin declarar ningún tipo.bail!(...)es azúcar dereturn Err(anyhow!(...)): aborta la función con ese error.ensure!(cond, ...)es el equivalente deassert!que devuelveErren vez de entrar enpanic!: si la condición es falsa, hacebail!.
Y ok_or_else(|| anyhow!(...)) es el puente idiomático para pasar de un Option ausente a un Result con mensaje.
Reportar desde main
El remate: si main devuelve anyhow::Result<()>, cuando algo falle Rust imprimirá el error mediante su Debug, y el Debug de anyhow despliega la cadena entera más el backtrace:
fn main() -> anyhow::Result<()> {
let umbral = leer_umbral("umbral.txt")?;
println!("umbral cargado: {umbral}");
Ok(())
}
Error: no se pudo leer 'umbral.txt'
Caused by:
No such file or directory (os error 2)
Con la variable de entorno RUST_BACKTRACE=1 se añade además la traza de la pila. Todo el informe “error … causado por …” que en la lección uno recorriste a mano con un bucle sobre source, aquí sale gratis.
anyhow es para binarios. Si expones anyhow::Result en la API pública de una librería, condenas a tus usuarios a la opacidad: no podrán hacer match sobre tus fallos y arrastrarán anyhow a su árbol de dependencias les guste o no. En una librería, devuelve un error tipado con thiserror. Reserva anyhow para el crate binario, o para funciones internas cuyo error nadie externo llega a ver.
flowchart TD A[read_to_string falla con io Error] -->|with_context| B[anyhow Error con una frase de contexto] B -->|interrogacion sube| C[La funcion propaga sin escribir From] C --> D[main devuelve anyhow Result] D --> E[Se imprime la cadena completa y el backtrace] style A fill:#f38ba8,color:#11111b style B fill:#cba6f7,color:#11111b style D fill:#89b4fa,color:#11111b style E fill:#a6e3a1,color:#11111b
anyhow parece lo contrario de todo lo que Rust predica —convierte errores tipados en un tipo opaco, borra información que tanto costó modelar— y sin embargo es idiomático. La aparente contradicción se disuelve al entender que el valor de la información de tipo no es absoluto, sino relativo a si alguien va a usarla. La identidad concreta de un error solo sirve si un consumidor aguas abajo va a decidir en función de ella: reintentar ante un error de red, ignorar un fichero ausente, distinguir un formato inválido. Una librería tiene ese consumidor —código ajeno que ramifica— y por eso le debe errores tipados. Pero la capa superior de una aplicación no lo tiene: sus errores suben hasta main, se registran y matan el proceso; jamás se hace match sobre ellos. En ese contexto, conservar el tipo es cargar con un peso que nadie levantará, mientras que la información que sí importa —qué estabas haciendo cuando falló— es justamente la que un enum no captura y context sí. Por eso anyhow no solo es aceptable en un binario, sino mejor que un enum hecho a mano: su diagnóstico es más rico —cadena completa más backtrace— y su ergonomía de propagación es total —? sin un solo From—. El trueque que ofrece, “ya no puedes hacer match”, no es una pérdida donde nadie iba a hacer match. La lección trasciende las crates: elegir entre errores tipados y opacos es en realidad elegir quién está aguas abajo. Si es código que razona, tipa; si es un humano que lee un log, narra. thiserror sirve a lo primero, anyhow a lo segundo, y saber en qué capa estás es saber cuál usar.
anyhow::Result<T> alias Result<T, anyhow::Error>; anyhow::Error absorbe cualquier Error + Send + Sync + 'static vía ?, sin From escritos. context y with_context (esta última perezosa) añaden eslabones a la cadena de causas. bail! aborta, ensure! es el assert! que devuelve Err, anyhow! fabrica un error desde un mensaje. main que devuelve anyhow::Result imprime la cadena entera y el backtrace. Es la herramienta de los binarios; nunca la pongas en la API pública de una librería.
- Escribe una función que lea un fichero y parsee su contenido a un número, devolviendo
anyhow::Result<u32>y añadiendowith_contexta cada paso. Provoca ambos fallos y compara los mensajes. - Convierte
mainenfn main() -> anyhow::Result<()>y observa el informe “Caused by” cuando el fichero no existe. ActivaRUST_BACKTRACE=1y compara. - Sustituye un
if ... { return Err(...) }porbail!y una comprobación previa porensure!. Explica qué azúcar desazucara cada macro. - Usa
ok_or_else(|| anyhow!(...))para convertir la búsqueda ausente en unHashMapen un error con mensaje. - Argumenta por qué sería un error de diseño devolver
anyhow::Resultdesde una función de una librería que otros crates van a consumir, y qué usarías en su lugar.