En esta página

Manejo de errores

12 min lectura TextoCap. 3 — Datos y errores

Manejo de errores en Go: explícito y robusto

El manejo de errores es uno de los aspectos más debatidos y a la vez más poderosos de Go. Go no tiene excepciones. En lugar de try/catch, los errores son simplemente valores que las funciones retornan. Esto obliga a que el manejo de errores sea explícito y visible en el código.

La crítica más común de Go es "hay demasiado if err != nil". La respuesta de la comunidad Go es que esta visibilidad es una característica, no un bug: nunca puedes ignorar accidentalmente un error. En Java, puedes olvidar un bloque catch y el error se propaga silenciosamente. En Go, el compilador te obliga a manejar cada error.

La interfaz `error`

En Go, error es simplemente una interfaz de la biblioteca estándar:

type error interface {
    Error() string
}

Cualquier tipo que tenga un método Error() string es un error. Esto hace que crear errores personalizados sea sencillo.

Creando errores simples

import "errors"

// errors.New — el más simple
var ErrSimple = errors.New("algo salió mal")

// fmt.Errorf — con formato
func validarEdad(edad int) error {
    if edad < 0 {
        return fmt.Errorf("edad inválida: %d (debe ser >= 0)", edad)
    }
    if edad > 150 {
        return fmt.Errorf("edad inválida: %d (máximo 150)", edad)
    }
    return nil
}

El patrón idiomático de manejo de errores

// Función que puede fallar retorna (resultado, error)
func leerArchivo(ruta string) ([]byte, error) {
    f, err := os.Open(ruta)
    if err != nil {
        return nil, fmt.Errorf("leerArchivo: %w", err)
    }
    defer f.Close()

    datos, err := io.ReadAll(f)
    if err != nil {
        return nil, fmt.Errorf("leerArchivo — lectura: %w", err)
    }

    return datos, nil
}

// Caller: siempre verificar el error antes de usar el resultado
datos, err := leerArchivo("config.json")
if err != nil {
    log.Fatal("no se pudo leer la configuración:", err)
}
// Solo aquí es seguro usar datos
procesarDatos(datos)

Errores centinela: valores predefinidos comparables

Un error centinela es una variable de error predefinida a nivel de paquete, comparable con == o errors.Is:

// Definición de errores centinela
var (
    ErrNoEncontrado     = errors.New("no encontrado")
    ErrSinAutorización  = errors.New("sin autorización")
    ErrConflicto        = errors.New("conflicto de recursos")
    ErrInválido         = errors.New("parámetro inválido")
)

// Uso
func obtenerProducto(id int) (*Producto, error) {
    if id <= 0 {
        return nil, fmt.Errorf("obtenerProducto: %w", ErrInválido)
    }
    p, existe := db[id]
    if !existe {
        return nil, fmt.Errorf("obtenerProducto id=%d: %w", id, ErrNoEncontrado)
    }
    return p, nil
}

// Comparar con errors.Is (atraviesa el wrapping)
p, err := obtenerProducto(0)
if errors.Is(err, ErrInválido) {
    fmt.Println("ID inválido:", err)
} else if errors.Is(err, ErrNoEncontrado) {
    fmt.Println("Producto no existe:", err)
}

Los errores centinela son convención en la biblioteca estándar: io.EOF, sql.ErrNoRows, os.ErrNotExist, etc.

Errores personalizados con datos

Cuando necesitas llevar información adicional en el error, crea un tipo personalizado:

// Error con código HTTP
type ErrorHTTP struct {
    Código   int
    Mensaje  string
    Detalle  string
}

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

// Error de validación con múltiples campos
type ErroresValidación struct {
    Campos map[string][]string
}

func (e *ErroresValidación) Error() string {
    return fmt.Sprintf("validación falló: %d campo(s) con errores", len(e.Campos))
}

func (e *ErroresValidación) Agregar(campo, mensaje string) {
    if e.Campos == nil {
        e.Campos = make(map[string][]string)
    }
    e.Campos[campo] = append(e.Campos[campo], mensaje)
}

func (e *ErroresValidación) TieneErrores() bool {
    return len(e.Campos) > 0
}

`fmt.Errorf` con `%w`: wrapping de errores

El operador %w en fmt.Errorf crea un error que envuelve al original, preservando la cadena de errores:

func operaciónA() error {
    return errors.New("error de base de datos")
}

func operaciónB() error {
    if err := operaciónA(); err != nil {
        return fmt.Errorf("operaciónB falló: %w", err)  // envuelve el error
    }
    return nil
}

func operaciónC() error {
    if err := operaciónB(); err != nil {
        return fmt.Errorf("operaciónC falló: %w", err)  // envuelve de nuevo
    }
    return nil
}

// La cadena completa de errores
err := operaciónC()
fmt.Println(err)
// "operaciónC falló: operaciónB falló: error de base de datos"

`errors.Is`: verificar errores en la cadena

errors.Is recorre la cadena de wrapping buscando un error específico:

var ErrOriginal = errors.New("error original")

err := fmt.Errorf("contexto: %w", fmt.Errorf("más contexto: %w", ErrOriginal))

// errors.Is atraviesa la cadena
fmt.Println(errors.Is(err, ErrOriginal))  // true

// == solo compara el error superficial
fmt.Println(err == ErrOriginal)  // false

`errors.As`: extraer el tipo concreto de la cadena

errors.As recorre la cadena de wrapping buscando un error de un tipo específico:

type ErrorBD struct {
    Query string
    Causa error
}

func (e *ErrorBD) Error() string {
    return fmt.Sprintf("query %q falló: %v", e.Query, e.Causa)
}

func (e *ErrorBD) Unwrap() error {
    return e.Causa
}

// En alguna función profunda
err := fmt.Errorf("capa servicio: %w", &ErrorBD{
    Query: "SELECT * FROM usuarios",
    Causa: errors.New("timeout"),
})

// Extraer el ErrorBD desde cualquier punto de la cadena
var errBD *ErrorBD
if errors.As(err, &errBD) {
    fmt.Printf("Query que falló: %s\n", errBD.Query)
}

Implementar `Unwrap()` para errores personalizados

Para que tu error funcione con errors.Is y errors.As, implementa Unwrap():

type ErrorContexto struct {
    Mensaje string
    Causa   error
}

func (e *ErrorContexto) Error() string {
    return fmt.Sprintf("%s: %v", e.Mensaje, e.Causa)
}

// Unwrap permite que errors.Is y errors.As traversen la cadena
func (e *ErrorContexto) Unwrap() error {
    return e.Causa
}

`panic` y `recover`: el último recurso

panic es para situaciones verdaderamente excepcionales — violaciones de invariantes del programa, condiciones que nunca deberían ocurrir en producción:

func dividir(a, b int) int {
    if b == 0 {
        panic("división por cero: invariante violado")
    }
    return a / b
}

// recover captura un panic — solo útil en defer
func manejarPanic() {
    defer func() {
        if r := recover(); r != nil {
            fmt.Println("Panic recuperado:", r)
        }
    }()

    panic("algo muy malo")
    // Nunca se ejecuta:
    fmt.Println("esto no se imprime")
}

El uso legítimo más común de recover es en los servidores HTTP para evitar que un pánico en un handler mate todo el servidor. La biblioteca estándar de Go hace esto internamente.

Errores en librerías vs. aplicaciones

  • Librerías: retorna errores ricos, nunca llames log.Fatal ni os.Exit
  • Aplicaciones: puedes usar log.Fatal(err) cuando el error es irrecuperable al inicio
// En una librería
func Conectar(dsn string) (*BD, error) {
    // ... retorna error al caller para que decida qué hacer
}

// En main de una aplicación
bd, err := milib.Conectar(dsn)
if err != nil {
    log.Fatalf("no se pudo conectar a la base de datos: %v", err)
}

Con el manejo de errores dominado, en la siguiente lección entraremos al corazón de Go: las goroutines — la forma en que Go hace que la concurrencia sea accesible y eficiente.

Usa %w para envolver errores y preservar la cadena
fmt.Errorf("contexto: %w", err) crea un nuevo error que envuelve al original. errors.Is() y errors.As() pueden atravesar esta cadena para encontrar errores específicos. Sin %w (usando %v), el error original se pierde y solo queda el mensaje como string.
panic/recover es para errores no recuperables, no para flujo normal
panic() en Go es para situaciones verdaderamente inesperadas: violaciones de invariantes, errores de programación, o cuando continuar sería peligroso. No uses panic como reemplazo de errores. La mayoría del código Go nunca debería llamar panic ni recover explícitamente.