El tipo error en Go: crear errores, envolverlos y comprobarlos bien

Go no tiene excepciones. El mecanismo para gestionar fallos es el tipo error, una interfaz con un único método. Esta simplicidad fuerza a los programadores a tratar los errores donde ocurren, hace el flujo de control explícito y evita los bloques try/catch que en otros lenguajes ocultan la propagación de errores.

La interfaz error

type error interface {
    Error() string
}

Cualquier tipo que implemente Error() string es un error. Puedes crear el tuyo propio o usar los helpers del paquete estándar.

errors.New y fmt.Errorf

import (
    "errors"
    "fmt"
)

var ErrDivisionPorCero = errors.New("división por cero")

func dividir(a, b float64) (float64, error) {
    if b == 0 {
        return 0, ErrDivisionPorCero
    }
    return a / b, nil
}

func calcular(a, b float64) (float64, error) {
    resultado, err := dividir(a, b)
    if err != nil {
        return 0, fmt.Errorf("calcular(%v, %v): %w", a, b, err)
    }
    return resultado, nil
}

El verbo %w de fmt.Errorf envuelve el error original: añade contexto pero conserva el error interior para que se pueda recuperar.

errors.Is: comparar errores a través de capas

errors.Is desenvuelve la cadena de errores y compara con el destino. Funciona aunque el error haya sido envuelto varias veces:

resultado, err := calcular(10, 0)
if errors.Is(err, ErrDivisionPorCero) {
    fmt.Println("es un error de división por cero")
    // imprime aunque el error fue envuelto con contexto
}
fmt.Println(err)
// calcular(10, 0): división por cero

No uses err == ErrDivisionPorCero: si el error fue envuelto, la comparación directa falla.

errors.As: extraer un tipo de error concreto

Cuando el error tiene campos adicionales (código HTTP, número de línea, nombre de fichero...) necesitas errors.As para convertirlo al tipo concreto:

type ErrorValidacion struct {
    Campo   string
    Mensaje string
}

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

func validar(nombre string) error {
    if len(nombre) == 0 {
        return fmt.Errorf("entrada inválida: %w", &ErrorValidacion{
            Campo:   "nombre",
            Mensaje: "no puede estar vacío",
        })
    }
    return nil
}

func main() {
    err := validar("")
    var ev *ErrorValidacion
    if errors.As(err, &ev) {
        fmt.Printf("Campo afectado: %sn", ev.Campo)
        fmt.Printf("Mensaje: %sn", ev.Mensaje)
    }
}

Crear tipos de error personalizados

type ErrorHTTP struct {
    Codigo  int
    Mensaje string
}

func (e *ErrorHTTP) Error() string {
    return fmt.Sprintf("HTTP %d: %s", e.Codigo, e.Mensaje)
}

func obtenerUsuario(id int) (*Persona, error) {
    if id <= 0 {
        return nil, &ErrorHTTP{Codigo: 400, Mensaje: "id inválido"}
    }
    if id == 99 {
        return nil, &ErrorHTTP{Codigo: 404, Mensaje: "usuario no encontrado"}
    }
    return &Persona{Nombre: "Ana"}, nil
}

El patrón sentinel error

Un sentinel error es una variable exportada de tipo error que representa un estado concreto conocido. Los paquetes de la biblioteca estándar los usan ampliamente:

// io.EOF indica que no hay más datos que leer
// sql.ErrNoRows indica que la consulta no devolvió filas

if err == io.EOF {
    // llegamos al final, no es un error real
}
if errors.Is(err, sql.ErrNoRows) {
    // el registro no existe
}

COMPARTE ESTE ARTÍCULO

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