wandres.dev
ERRORES PROPIOS · thiserror y anyhow

thiserror: el boilerplate de errores, derivado

En una librería tus errores son parte de la API y deben seguir tipados. thiserror deriva Display, Error y las conversiones From a partir de una declaración anotada, sin coste en ejecución ni pérdida de tipo. El derive que industrializa las lecciones anteriores.

⏱ 18 min

Escribir a mano Display, source y un impl From por variante es correcto pero tedioso, y el tedio invita al error humano. En una librería no puedes renunciar al tipado —tus errores son parte del contrato público que tus usuarios diseccionan con match— así que tampoco puedes escapar hacia Box<dyn Error>. La salida es thiserror: una derive macro que genera todo ese boilerplate a partir de una declaración anotada. No introduce ninguna abstracción nueva, no borra tu tipo, no cuesta un solo ciclo en ejecución: emite exactamente el código que habrías tecleado, pero deducido de unos atributos. Es la herramienta canónica para errores de librería en Rust 2024.

🎯 Al terminar esta lección sabrás
  • Derivar Display, Error y From con #[derive(thiserror::Error)].
  • Redactar mensajes con #[error("...")] interpolando campos posicionales y con nombre.
  • Generar conversiones para ? con #[from] y marcar la causa con #[source].
  • Delegar mensaje y causa a un error envuelto con #[error(transparent)].

El derive que escribe Display, Error y From

Se añade la dependencia y se anota el enum. La versión vigente en 2026 es la 2:

[dependencies]
thiserror = "2"
use thiserror::Error;

#[derive(Debug, Error)]
pub enum ErrorAlmacen {
    #[error("clave no encontrada: {0}")]
    NoEncontrada(String),

    #[error("cuota superada: {usadas} de {limite} entradas")]
    CuotaSuperada { usadas: usize, limite: usize },

    #[error("fallo de entrada/salida en el disco")]
    Io(#[from] std::io::Error),

    #[error("indice corrupto")]
    Indice(#[source] std::num::ParseIntError),
}

Ese bloque genera, en tiempo de compilación, tres cosas que antes escribías a mano: el impl Display completo (a partir de las cadenas #[error(...)]), el impl std::error::Error (con un source que apunta a los campos marcados), y un impl From<std::io::Error> for ErrorAlmacen (por el #[from]). Tú solo sigues derivando Debug, que thiserror deja en tus manos. El resultado es un enum normal y corriente: se puede match, es exhaustivo, es tu tipo. thiserror no lo envuelve ni lo oculta.

Mensajes, from y source con atributos

Cada atributo tiene un cometido preciso, y conviene no confundirlos:

  • #[error("...")] define el Display de esa variante. Dentro de la cadena, {0} interpola el campo posicional cero de una variante-tupla y {usadas} interpola un campo con nombre. Admite también especificadores de formato, como {limite:>4}.
  • #[from] sobre un campo hace dos cosas de golpe: genera el impl From de ese tipo hacia tu enum —lo que permite que ? convierta hacia esta variante— e implica #[source], marcando ese campo como la causa. Solo puede haber un tipo por cada #[from]: no puedes tener dos variantes con #[from] std::io::Error, porque generarían dos From en conflicto.
  • #[source] marca un campo como la causa (lo expone source) sin generar ninguna conversión. Se usa cuando quieres el hilo de causas pero no quieres que ? convierta automáticamente hacia esa variante.

Para dimensionar el ahorro, compara. El ErrorCarga que escribiste a mano en la lección anterior —dos match y dos From, unas veinte líneas— se colapsa a esto, con idéntico comportamiento:

#[derive(Debug, thiserror::Error)]
enum ErrorCarga {
    #[error("fallo al leer el fichero")]
    Lectura(#[from] std::io::Error),
    #[error("el contenido no es un numero")]
    Formato(#[from] std::num::ParseIntError),
}

Mismo tipo, mismo Display, mismo source, mismas conversiones para ?, un tercio del código y ni una oportunidad de teclear mal un match.

💡
Redacta el mensaje sin punto final ni mayúscula

Por convención, los mensajes de Display de un error se escriben en minúscula y sin punto final: son fragmentos que se encadenan. Cuando una herramienta imprima error: {tuyo}: causado por: {la causa}, tu frase debe encajar en medio sin chocar. Reserva la mayúscula y la puntuación para el mensaje final que compone la aplicación, no para cada eslabón.

transparent: el caso de reenvío puro

A veces una variante no aporta mensaje propio: es un simple pasamanos hacia otro error, y quieres que su Display y su source sean los del error envuelto, no una capa redundante encima. Para eso está #[error(transparent)]:

#[derive(Debug, thiserror::Error)]
pub enum ErrorApi {
    #[error("la peticion excede el limite de tamano")]
    DemasiadoGrande,

    #[error(transparent)]
    Red(#[from] std::io::Error),
}

La variante Red no inventa una frase: delega Display y source directamente al std::io::Error que contiene. Se usa cuando reexportas el error de una capa inferior sin nada útil que añadir; poner ahí un mensaje genérico como “error de red” solo estorbaría al lector, que prefiere ver el mensaje real del sistema. La regla: transparent exige que la variante tenga exactamente un campo, el que absorbe toda la representación.

flowchart TD
D[Declaras un enum con derive Error] --> M[La macro genera en compilacion]
M --> A[impl Display desde los atributos error]
M --> B[impl Error con source]
M --> C[impl From por cada campo from]
A --> R[Tu tipo sigue siendo un enum normal y matchable]
B --> R
C --> R
style D fill:#cba6f7,color:#11111b
style M fill:#89b4fa,color:#11111b
style R fill:#a6e3a1,color:#11111b
La ergonomía del autor de librerías nace del codegen, no del borrado de tipo

thiserror merece una mirada por lo que revela del alma de Rust. Una derive macro es, en esencia, una función que se ejecuta durante la compilación: recibe la declaración de tu tipo como dato y devuelve más código Rust. thiserror es esa función aplicada al álgebra de errores: tú declaras la forma de tus fallos —qué variantes hay, qué datos llevan, qué frase dice cada una— y la macro escribe la mecánica que conecta esa forma con los traits del ecosistema. Es el mismo principio que hace posible #[derive(Debug)] o la implementación general de Into: una capacidad que viaja adosada a una declaración, sin que nadie transcriba el pegamento a mano. Pero el rasgo decisivo es lo que thiserror no hace. No introduce un tipo envoltorio, no mete indirección, no recurre a reflexión en tiempo de ejecución, no borra tu tipo. El código que emite es, línea por línea, el que habrías escrito tú: tu error sigue siendo un enum desnudo, tipado, exhaustivo, sobre el que tus usuarios hacen match como sobre cualquier otro. Ahí está la lección profunda: en Rust, la comodidad para el autor de una librería no se compra sacrificando garantías del sistema de tipos ni pagando coste en ejecución, sino trasladando trabajo mecánico al compilador. No pagas nada en runtime y no pierdes nada en expresividad; solo te desprendes de la transcripción. Por eso thiserror es la respuesta correcta para las librerías —donde el tipo importa— y por eso, en la lección siguiente, anyhow podrá ser la respuesta opuesta para los binarios sin contradecirla: son dos usos distintos de la misma maquinaria de conversión.

📝
Lo esencial de thiserror

#[derive(thiserror::Error)] genera Display (desde #[error("...")]), Error (con source) y From (desde #[from]), dejándote derivar Debug. #[error("...")] interpola {0} y {campo}; #[from] crea la conversión para ? e implica #[source]; #[source] marca la causa sin conversión; #[error(transparent)] delega mensaje y causa al único campo envuelto. Coste en ejecución: cero. Tu tipo sigue siendo un enum tipado y matchable: por eso es la herramienta de las librerías.

⚔️ Deja que la macro escriba el pegamento
  1. Reescribe con #[derive(thiserror::Error)] el enum ErrorPedido de la lección anterior. Comprueba que el Display generado coincide con el que escribiste a mano.
  2. Añade una variante Io(#[from] std::io::Error) y escribe una función que use ? sobre una operación de fichero; verifica que la conversión ocurre sin ningún impl From tuyo.
  3. Distingue en la práctica #[from] de #[source]: crea una variante con #[source] e intenta usar ? para convertir hacia ella. Explica por qué no compila y qué tendrías que hacer.
  4. Usa #[error(transparent)] en una variante que envuelva un std::io::Error y contrasta su Display con el de una variante que sí ponga un mensaje propio.
  5. Provoca un conflicto declarando dos variantes con #[from] std::io::Error. Lee el error del compilador y razona por qué la conversión no puede ser ambigua.