Creación y wrapping de errores en Go

En Go, los errores son valores, no excepciones. Esto significa que tratas a los errores como cualquier otra variable que fluye a través de tus funciones. Para trabajar con ellos de forma profesional, necesitas entender cómo crearlos y, sobre todo, cómo añadirles contexto sin romper la capacidad de inspección del error original.

Para crear un error estático, usamos errors.New. Sin embargo, hay un detalle crítico: errors.New devuelve un puntero a una estructura interna. Esto implica que, aunque llames a errors.New("error") dos veces con el mismo string, obtendrás dos instancias diferentes en memoria. Si intentas compararlas con ==, la comparación será false. Por eso, para errores que quieres que otros paquetes identifiquen (llamados sentinel errors), debes declararlos como variables de paquete.

Cuando ya tienes un error y quieres añadirle información (como el ID de un usuario o una ruta de archivo) sin perder la identidad del error original, tienes dos caminos con fmt.Errorf. Si usas %v, simplemente formateas el mensaje y creas un nuevo error que “olvida” su origen; el mensaje será descriptivo, pero la cadena de error se corta ahí. Si usas %w [disponible desde Go 1.13], estás realizando un wrapping. Esto implementa internamente un método Unwrap que permite a las funciones errors.Is y errors.As navegar por la jerarquía de errores para encontrar el origen real.

Usa errors.New para definir errores globales y estáticos en tus paquetes. Usa fmt.Errorf con %w siempre que quieras envolver un error que viene de una capa inferior (como la base de datos o un cliente HTTP) para dar contexto de dónde ocurrió el fallo. Evita usar %v para errores que el llamante podría necesitar identificar mediante errors.Is. Si te equivocas y usas %v para envolver un error, el código que te llama no podrá distinguir entre un error de “registro no encontrado” y un “error de conexión”, porque para la runtime, el error original simplemente ha desaparecido, dejando solo un string.

package main

import (
	"errors"
	"fmt"
)

// Definimos errores "sentinels". Se declaran como variables globales 
// para que otros paquetes puedan compararlos con `errors.Is`.
var (
	ErrUserNotFound = errors.New("usuario no encontrado")
	ErrDBConnection = errors.New("error de conexión a la base de datos")
)

type User struct {
	ID   int
	Name string
}

// queryDB simula una llamada a una base de datos.
func queryDB(id int) (*User, error) {
	if id == 0 {
		return nil, ErrDBConnection
	}
	if id > 100 {
		return nil, ErrUserNotFound
	}
	return &User{ID: id, Name: "Carlos"}, nil
}

// getUser añade contexto al error usando %w para permitir la inspección posterior.
func getUser(id int) (*User, error) {
	user, err := queryDB(id)
	if err != nil {
		// Usamos %w para envolver el error original. Esto crea una cadena de errores.
		return nil, fmt.Errorf("servicio de usuarios (id: %d): %w", id, err)
	}
	return user, nil
}

func main() {
	// Caso 1: El error es un "sentinel" envuelto con contexto.
	_, err := getUser(150)
	if err != nil {
		fmt.Printf("Error capturado: %v\n", err)

		// errors.Is recorre la cadena de errores gracias al %w usado arriba.
		if errors.Is(err, ErrUserNotFound) {
			fmt.Println("Resultado: El usuario no existe (lógica de negocio).")
		} else if errors.Is(err, ErrDBConnection) {
			fmt.Println("Resultado: Intenta reconectar (error de infraestructura).")
		} else {
			fmt.Println("Resultado: Error desconocido.")
		}
	}

	// Caso 2: Error que NO es un sentinel (un error genérico de formato).
	errGenérico := fmt.Errorf("error inesperado: %v", ErrUserNotFound)
	
	// Esto fallará porque usamos %v, rompiendo la cadena de inspección.
	if errors.Is(errGenérico, ErrUserNotFound) {
		fmt.Println("Esto no se imprimirá.")
	} else {
		fmt.Println("Error genérico: No se puede identificar el origen con errors.Is")
	}
}

Desglose del ejemplo

En el código, ErrUserNotFound es un error predefinido. Si dentro de queryDB retornamos este valor, estamos entregando una referencia específica.

En getUser, cuando llamamos a fmt.Errorf("... %w", id, err), no estamos simplemente concatenando strings. Estamos creando un nuevo tipo de error que encapsula a err. Gracias a esto, cuando en main ejecutamos errors.Is(err, ErrUserNotFound), la función errors.Is no solo mira si err es igual a ErrUserNotFound, sino que pregunta: “¿Contiene este error, en su interior, el error ErrUserNotFound?”. Como usamos %w, la respuesta es sí.

En el segundo caso de main, cometemos un error de diseño al usar %v. Aunque el string resultante contenga el texto “usuario no encontrado”, la estructura del objeto error es plana. errors.Is no tiene forma de “entrar” en el error para ver qué causó ese mensaje, por lo que la inspección falla.

El error frecuente

El error más común en sistemas de producción es el “silenciamiento de la jerarquía” mediante el uso de %v en lugar de %w.

// MAL: Pierdes la capacidad de usar errors.Is(err, ErrNotFound)
if err != nil {
    return fmt.Errorf("falló la operación: %v", err)
}

// BIEN: Mantienes la capacidad de inspeccionar la causa raíz
if err != nil {
    return fmt.Errorf("falló la operación: %w", err)
}

Si estás construyendo una librería, usa %w casi siempre. Si estás escribiendo un cliente que solo quiere reportar un error final sin que el usuario sepa los detalles internos de tu arquitectura, puedes usar %v.

89

Dejar un comentario

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

Scroll al inicio