Errores avanzados en Go: fmt.Errorf %w, errors.Is, errors.As y errores personalizados

Go 1.13 introdujo el envoltorio de errores con %w y las funciones errors.Is y errors.As, que permiten inspeccionar errores en cadena sin perder contexto. Con Go 1.20 llegó errors.Join para combinar múltiples errores en uno solo. Entender este modelo es fundamental para escribir código Go idiomático.

Envolver errores con fmt.Errorf %w

%w crea un error que envuelve al original, añadiendo contexto sin perder el tipo ni el valor del error interno:

package main

import (
    "errors"
    "fmt"
)

var ErrNoEncontrado = errors.New("no encontrado")

func buscarUsuario(id int) error {
    if id <= 0 {
        return fmt.Errorf("buscarUsuario: id inválido %d", id)
    }
    if id == 99 {
        return fmt.Errorf("buscarUsuario(id=%d): %w", id, ErrNoEncontrado)
    }
    return nil
}

func main() {
    err := buscarUsuario(99)
    fmt.Println(err) // buscarUsuario(id=99): no encontrado
}

errors.Is: comparar errores en la cadena

errors.Is recorre la cadena de errores envueltos buscando uno que coincida. Sustituye completamente la comparación directa con ==:

err := buscarUsuario(99)

// Comparación directa: falla porque err no ES ErrNoEncontrado, solo lo envuelve
fmt.Println(err == ErrNoEncontrado) // false

// errors.Is: recorre la cadena y lo encuentra
fmt.Println(errors.Is(err, ErrNoEncontrado)) // true

// Patrón idiomático para manejar casos específicos
if errors.Is(err, ErrNoEncontrado) {
    fmt.Println("el usuario no existe, creando uno nuevo...")
}

errors.As: extraer el tipo de error

errors.As recorre la cadena buscando un error del tipo especificado y lo asigna a la variable destino:

type ErrorValidacion struct {
    Campo   string
    Mensaje string
}

func (e *ErrorValidacion) Error() string {
    return fmt.Sprintf("validación fallida en %s: %s", e.Campo, e.Mensaje)
}

func validarEmail(email string) error {
    if !strings.Contains(email, "@") {
        return &ErrorValidacion{Campo: "email", Mensaje: "falta el símbolo @"}
    }
    return nil
}

func procesarFormulario(email string) error {
    if err := validarEmail(email); err != nil {
        return fmt.Errorf("formulario inválido: %w", err)
    }
    return nil
}

func main() {
    err := procesarFormulario("no-es-un-email")

    var errVal *ErrorValidacion
    if errors.As(err, &errVal) {
        fmt.Printf("campo: %s, problema: %sn", errVal.Campo, errVal.Mensaje)
    }
}

Implementar Unwrap en tipos propios

Para que tus errores personalizados participen en la cadena de errors.Is y errors.As, implementa el método Unwrap:

type ErrorBD struct {
    Operacion string
    Original  error
}

func (e *ErrorBD) Error() string {
    return fmt.Sprintf("BD[%s]: %v", e.Operacion, e.Original)
}

func (e *ErrorBD) Unwrap() error {
    return e.Original // expone el error interno
}

var ErrConexionPerdida = errors.New("conexión perdida")

func consultarBD() error {
    return &ErrorBD{
        Operacion: "SELECT",
        Original:  ErrConexionPerdida,
    }
}

func main() {
    err := consultarBD()
    fmt.Println(errors.Is(err, ErrConexionPerdida)) // true, gracias a Unwrap
}

errors.Join: combinar múltiples errores (Go 1.20)

errors.Join combina varios errores en uno solo. errors.Is y errors.As inspeccionan todos los errores incluidos:

func validarPedido(p Pedido) error {
    var errs []error
    if p.Total <= 0 {
        errs = append(errs, errors.New("el total debe ser positivo"))
    }
    if p.UsuarioID == 0 {
        errs = append(errs, errors.New("el usuario es obligatorio"))
    }
    if len(p.Items) == 0 {
        errs = append(errs, errors.New("el pedido debe tener al menos un ítem"))
    }
    return errors.Join(errs...) // nil si errs está vacío
}

func main() {
    err := validarPedido(Pedido{})
    if err != nil {
        fmt.Println(err)
        // el total debe ser positivo
        // el usuario es obligatorio
        // el pedido debe tener al menos un ítem
    }
}

Errores centinela vs tipos propios

  • Usa errores centinela (var ErrX = errors.New("...")) cuando solo importa si el error ocurrió, no los detalles.
  • Usa tipos propios cuando necesitas campos adicionales como el código HTTP, el campo que falló o la operación que se estaba ejecutando.
  • Nunca uses panic para errores recuperables: reserva el panic para bugs del programa, no para entradas inválidas del usuario.

COMPARTE ESTE ARTÍCULO

COMPARTIR EN FACEBOOK
COMPARTIR EN TWITTER
COMPARTIR EN LINKEDIN
COMPARTIR EN WHATSAPP