errores-go-patron-if-err-nil-custom

Gestión de errores en Go con if err != nil y wrapping

  • 6 min

Un error en Go es un valor que representa que una operación no ha podido completarse correctamente. No se lanza por los aires: se devuelve y se revisa.

Si vienes de lenguajes como Java, C#, Python o JavaScript, tu memoria muscular está entrenada para buscar el bloque try-catch. Estás acostumbrado a que, cuando algo va mal, el programa lanza una excepción que “burbujea” hacia arriba hasta que alguien la captura.

En Go, no existen las excepciones (bueno, existe panic, pero no es para lo que crees).

En Go, los errores no son eventos catastróficos que rompen el flujo: son valores. Un error es un dato más, igual que un entero o un string, que tu función devuelve y que tú tienes la responsabilidad de gestionar explícitamente.

Vamos a ver el patrón if err != nil, los errores personalizados y cómo conservar su identidad al añadir contexto.

La interfaz error

Todo el sistema de errores de Go se basa en una interfaz sencillísima incorporada en el lenguaje:

type error interface {
    Error() string
}
Copied!

Cualquier cosa que tenga un método Error() que devuelva un string, es un error. Así de simple.

El valor cero de una interfaz es nil. Por tanto, si una función devuelve un error igual a nil, significa que todo ha ido bien.

Una interfaz solo es nil si no contiene ni tipo dinámico ni valor. Si devolvemos como error un puntero de error con valor nil, la interfaz conserva su tipo y no será igual a nil. Por eso debemos devolver nil de forma explícita cuando no hay error.

El patrón if err != nil

Como vimos en el bloque de funciones, Go permite el retorno múltiple. El patrón estándar es que una función devuelva (resultado, error).

Intentamos ejecutar la lógica.

Comprobamos inmediatamente si ha ocurrido un error.

Si hay error, lo manejamos (logueamos, retornamos, reintentamos…).

Si no hay error, continuamos con el flujo principal.

package main

import (
    "fmt"
    "os"
)

func main() {
    // Intentamos abrir un archivo
    archivo, err := os.Open("config.json")

    // COMPROBACIÓN EXPLÍCITA
    if err != nil {
        // Aquí manejamos el problema
        fmt.Println("Oye, ha fallado la apertura:", err)
        return // Importante: Salimos de la función
    }

    // Si llegamos aquí, garantizamos que 'err' es nil y 'archivo' es válido
    defer archivo.Close()
    fmt.Println("Archivo abierto con éxito")
}
Copied!

Esta forma de trabajar hace que el flujo de control sea transparente. Al leer el código, sabes exactamente dónde puede fallar cada línea. No hay excepciones saltando 20 capas hacia arriba sin que las veas pasar.

Creación de errores básicos

¿Cómo generamos nuestros propios errores? Tenemos dos formas principales en la librería estándar.

errors.New para un mensaje estático

Para errores simples que solo necesitan un texto fijo. Necesitas importar el paquete errors.

import "errors"

func Dividir(a, b int) (int, error) {
    if b == 0 {
        return 0, errors.New("no se puede dividir por cero")
    }
    return a / b, nil
}
Copied!

fmt.Errorf para un mensaje dinámico

Si quieres incluir datos dentro del mensaje de error (como el ID que ha fallado), usamos fmt.Errorf. Funciona igual que Printf.

func BuscarUsuario(id int) (*User, error) {
    // ... lógica ...
    return nil, fmt.Errorf("el usuario con ID %d no existe", id)
}
Copied!

Errores personalizados

A veces un simple string no es suficiente. ¿Qué pasa si queremos devolver un código de error HTTP (404, 500) o un código de error interno para que quien llame a la función pueda reaccionar diferente?

Como error es una interfaz, podemos crear nuestro propio struct e implementar el método Error().

// Definimos nuestro tipo de error personalizado
type ErrorBaseDatos struct {
    Hora    string
    Mensaje string
    Codigo  int
}

// Implementamos la interfaz 'error'
func (e *ErrorBaseDatos) Error() string {
    return fmt.Sprintf("[%s] Error %d: %s", e.Hora, e.Codigo, e.Mensaje)
}

func ConectarBD() error {
    return &ErrorBaseDatos{
        Hora:    "12:00",
        Mensaje: "Conexión rechazada",
        Codigo:  503,
    }
}
Copied!

Ahora, quien llame a ConectarBD recibe un error, pero puede hacer una Aserción de Tipo (como vimos en el artículo anterior) para acceder al código 503.

Envolver errores para añadir contexto

Desde Go 1.13, la gestión de errores dio un salto de calidad con el concepto de Wrapping (envolver errores).

Supongamos que falla una consulta SQL.

  1. La capa de base de datos devuelve “Error de conexión”.
  2. La capa de negocio recibe ese error y quiere añadir contexto: “Fallo al obtener usuario: Error de conexión”.

Si usamos %v al construir un error nuevo, conservamos el mensaje, pero perdemos la identidad del error original. Para que los llamadores puedan inspeccionarlo, usamos %w.

errOriginal := errors.New("conexión perdida")
// Envolvemos el error
errConContexto := fmt.Errorf("fallo en query: %w", errOriginal)
Copied!

errors.Is (Comparar errores centinela)

No uses == para comparar errores si estos han sido envueltos. Usa errors.Is. Busca recursivamente dentro de las capas del error.

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

func main() {
    err := funcionQueDevuelveErrorEnvuelto()

    // ¿El error original, en el fondo, es un ErrNotFound?
    if errors.Is(err, ErrNotFound) {
        fmt.Println("Vale, es un 404, no pasa nada.")
    }
}
Copied!

errors.As para encontrar un tipo de error

Si usaste un Struct personalizado (como ErrorBaseDatos) y el error está envuelto, usa errors.As para extraerlo.

err := funcionQueFalla()

var miErrorBD *ErrorBaseDatos

// Intenta encontrar un ErrorBaseDatos dentro de la cadena de errores 'err'
// Si lo encuentra, lo guarda en 'miErrorBD' y devuelve true
if errors.As(err, &miErrorBD) {
    fmt.Println("El código de error de la BD es:", miErrorBD.Codigo)
}
Copied!

Desde Go 1.26 también podemos escribir errors.AsType[*ErrorBaseDatos](err), que devuelve el error encontrado y un booleano sin necesitar un puntero al destino.

Varios errores a la vez

errors.Join permite combinar varios errores conservando cada uno para errors.Is, errors.As y errors.AsType. Es útil, por ejemplo, cuando fallan varias tareas independientes y queremos devolver un único error que contenga todas las causas.

err := errors.Join(errLectura, errCierre)
Copied!

Los valores nil se descartan y, si todos son nil, errors.Join también devuelve nil.

Buenas prácticas al gestionar errores

  1. No ignores un error sin una razón concreta: si el resultado es irrelevante por contrato, documenta esa decisión; en los demás casos, manéjalo o devuélvelo.
  2. Maneja el error primero (Indentación a la izquierda): Mal (Else innecesario):
if err != nil {
    return err
} else {
    // código feliz...
}
Copied!

Bien (Early Return):

if err != nil {
    return err
}
// código feliz sin indentar
Copied!
  1. Añade contexto, no solo pases el error: Si una función falla, intenta envolver el error con fmt.Errorf("fallo al procesar pedido %d: %w", id, err) para que el log final cuente una historia completa.