Uso correcto de errors.Is y errors.As en Go

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.

91

Dejar un comentario

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *

Scroll al inicio