En Go, los errores no son solo cadenas de texto; son valores que pueden envolverse para añadir contexto a medida que un error escala por las capas de la aplicación. Cuando utilizas fmt.Errorf con el verbo %w [disponible desde Go 1.13], creas lo que conocemos como error wrapping, construyendo una cadena de errores donde el error original queda anidado dentro de uno nuevo. El problema surge cuando intentas inspeccionar esa cadena: una comparación directa con == fallará porque el nuevo error es una instancia distinta a la original, aunque contenga el error original en su interior. Para resolver esto, Go proporciona errors.Is y errors.As.
errors.Is se utiliza para buscar sentinel errors (errores predefinidos como io.EOF o os.ErrNotExist). En lugar de comparar identidades, errors.Is recorre la cadena de errores preguntando a cada nivel: “¿eres tú el error que busco?”. Por otro lado, errors.As se usa cuando necesitas extraer un error de un tipo específico para acceder a sus campos adicionales. No solo comprueba si el error es del tipo esperado, sino que, si lo es, “desenvuelve” el error y asigna el valor a una variable que puedes usar. Si intentas usar comparaciones de tipo tradicionales (err.(*MyError)) o comparaciones directas en una arquitectura que utiliza wrapping, tu lógica de recuperación de errores fallará silenciosamente, dejando de capturar excepciones críticas que han sido enriquecidas con contexto.
package main
import (
"errors"
"fmt"
"os"
)
// MyCustomError es un tipo de error personalizado que contiene metadatos.
type MyCustomError struct {
Code int
Message string
}
// Error implementa la interfaz error para MyCustomError.
func (e *MyCustomError) Error() string {
return fmt.Sprintf("código %d: %s", e.Code, e.Message)
}
// ErrDatabaseTimeout es un error sentinel para fallos de tiempo de espera.
var ErrDatabaseTimeout = errors.New("timeout de la base de datos")
func performQuery() error {
// Simulamos un error de timeout envuelto con contexto adicional.
return fmt.Errorf("operación de lectura fallida: %w", ErrDatabaseTimeout)
}
func openFile() error {
// Simulamos un error personalizado envuelto.
return fmt.Errorf("fallo en el sistema de archivos: %w", &MyCustomError{
Code: 404,
Message: "el recurso es inaccesible",
})
}
func main() {
// 1. Caso de error sentinel con errors.Is
errQuery := performQuery()
if errors.Is(errQuery, ErrDatabaseTimeout) {
fmt.Println("✅ Detectado: El error es un timeout (vía errors.Is)")
} else {
fmt.Println("❌ Error: No se detectó el timeout")
}
// 2. Caso de error de tipo personalizado con errors.As
errFile := openFile()
var myErr *MyCustomError
if errors.As(errFile, &myErr) {
// errors.As extrae el puntero y lo asigna a myErr
fmt.Printf("✅ Detectado: Error personalizado con código %d: %s (vía errors.As)\n", myErr.Code, myErr.Message)
}
// 3. Por qué la comparación directa falla con wrapping
if errQuery == ErrDatabaseTimeout {
// Este bloque nunca se ejecutará
fmt.Println("Esto no debería imprimirse")
} else {
fmt.Println("⚠️ La comparación directa con '==' falló porque el error está envuelto")
}
}
En el ejemplo anterior, la función performQuery devuelve un error generado por fmt.Errorf("... %w", ErrDatabaseTimeout). Esto no devuelve el objeto ErrDatabaseTimeout directamente, sino un nuevo objeto que implementa el método Unwrap(), el cual apunta al original. Cuando llamamos a errors.Is(errQuery, ErrDatabaseTimeout), la función recorre la estructura, encuentra el enlace mediante Unwrap() y confirma que el error base coincide con nuestro target.
En el caso de openFile, devolvemos un MyCustomError envuelto. Aquí no nos interesa saber si el error es “igual” a un valor, sino si es “del tipo” *MyCustomError para poder usar su campo Code. Al usar errors.As(errFile, &myErr), le pasamos la dirección de un puntero a nuestra variable myErr. errors.As recorre la cadena, identifica que el error anidado es de tipo *MyCustomError y realiza la asignación por nosotros. Si usáramos una aserción de tipo directa como err.(*MyCustomError), la operación fallaría porque errFile no es *MyCustomError, sino un error envuelto que contiene a *MyCustomError.
El error frecuente
Cuando trabajas con funciones que añaden contexto a los errores, la comparación directa de igualdad deja de ser válida.
// El error típico:
err := fmt.Errorf("error al abrir: %w", os.ErrNotExist)
if err == os.ErrNotExist {
// Este código NUNCA se ejecutará.
// 'err' es un objeto distinto que envuelve a 'os.ErrNotExist'.
handleNotFound()
}
// La forma correcta:
if errors.Is(err, os.ErrNotExist) {
handleNotFound()
}
Al usar %w, creas un nuevo nodo en la lista enlazada del error. La comparación == solo funciona para comparar la dirección de memoria de la instancia o el valor directo, pero no “navega” hacia el interior del error.
N° 91