El error wrapping es la técnica de envolver un error existente para añadirle información semántica sin perder la identidad del error original. En lugar de simplemente retornar un error, creamos uno nuevo que actúa como una capa superior, describiendo qué intentaba hacer la función cuando ocurrió el fallo. Esto se logra mediante fmt.Errorf utilizando el verbo %w.
Mecánicamente, cuando utilizas %w, el nuevo error implementa un método Unwrap() error que apunta al error original. Esto construye una estructura de árbol o cadena de errores. Para navegar por esta jerarquía, Go proporciona errors.Is (para comparar contra errores sentinela) y errors.As (para extraer tipos de errores específicos). Si usas el verbo %v o %s en lugar de %w, el error se convierte en una simple cadena de texto plana; esto rompe la cadena de inspección y hace que errors.Is falle al intentar buscar la causa raíz.
Debes usar esta técnica siempre que estés propagando un error desde una capa baja (como un driver de base de datos o un cliente HTTP) hacia una capa superior de lógica de negocio. Al añadir contexto en cada paso del stack, transformas un error críptico como connection refused en una traza comprensible: failed to start server: failed to connect to db: connection refused.
A partir de Go 1.20 [disponible desde Go 1.20], puedes envolver múltiples errores en una sola llamada si una operación falla por varias causas simultáneas, usando múltiples verbos %w en un mismo fmt.Errorf.
package main
import (
"errors"
"fmt"
)
// Errores sentinela para comparaciones de tipo error
var (
ErrDatabaseConnection = errors.New("database connection lost")
ErrConfigNotFound = errors.New("configuration file not found")
)
func main() {
err := runApplication()
if err != nil {
// Mostramos el error completo (toda la cadena)
fmt.Printf("Error final reportado: %v\n", err)
// errors.Is recorre toda la cadena de wrapping para encontrar la causa raíz
if errors.Is(err, ErrConfigNotFound) {
fmt.Println("-> Diagnóstico: El problema es que falta el archivo de configuración.")
}
if errors.Is(err, ErrDatabaseConnection) {
fmt.Println("-> Diagnóstico: El problema es la conexión a la base de datos.")
}
}
}
func runApplication() error {
errCfg := loadConfig()
errDB := connectToDB()
// Go 1.20+: Si ambos fallan, envolvemos ambos errores para no perder información
if errCfg != nil && errDB != nil {
return fmt.Errorf("startup failed: config error [%w] AND db error [%w]", errCfg, errDB)
}
if errCfg != nil {
// Envolvemos el error de configuración con contexto de nivel superior
return fmt.Errorf("failed to initialize application: %w", errCfg)
}
return errDB
}
func loadConfig() error {
// Simulamos un error de archivo, envolviendo el error sentinela
return fmt.Errorf("cannot find /etc/app/config.yaml: %w", ErrConfigNotFound)
}
func connectToDB() error {
// Retornamos directamente el error sentinela
return ErrDatabaseConnection
}
En el ejemplo, loadConfig no solo devuelve un error, sino que envuelve ErrConfigNotFound con un mensaje específico sobre la ruta del archivo. Esto crea el primer eslabón de la cadena. En runApplication, vemos la potencia de Go 1.20: si tanto la configuración como la base de datos fallan, el error resultante contiene ambas ramas de información. Cuando main llama a errors.Is(err, ErrConfigNotFound), el runtime no solo compara la identidad del error, sino que recorre toda la estructura de “capas” (llamando a Unwrap internamente) hasta encontrar que, en algún punto del árbol, el error original era nuestro sentinela.
El error frecuente
Un error común es utilizar %v cuando el objetivo es propagar un error para su inspección posterior:
// ERROR: Esto rompe la cadena de wrapping
return fmt.Errorf("failed to load: %v", err)
Si haces esto, err se convierte en un string dentro de otro string. Aunque el mensaje impreso sea el mismo, el nuevo error ya no implementa el método Unwrap(). Si intentas usar errors.Is(err, ErrConfigNotFound) sobre este error, recibirá un false, porque para el compilador el error es ahora una cadena de texto sin relación estructural con el original.
N° 90