← Volver al listado de tecnologías

Manejo de Errores

Por: Artiko
goerroreserror-handling

Go maneja errores de forma explicita mediante valores. No hay excepciones ni try/catch. Los errores son valores que implementan la interfaz error, y se retornan como cualquier otro valor.

La Interfaz error

La interfaz error es la mas simple de Go:

type error interface {
    Error() string
}

Cualquier tipo con un metodo Error() string es un error valido.

Crear Errores

Con errors.New

Para errores simples sin formato:

import "errors"

func dividir(a, b float64) (float64, error) {
    if b == 0 {
        return 0, errors.New("division por cero")
    }
    return a / b, nil
}

func main() {
    resultado, err := dividir(10, 0)
    if err != nil {
        fmt.Println("Error:", err)
        return
    }
    fmt.Println(resultado)
}

Con fmt.Errorf

Para errores con contexto formateado:

func buscarUsuario(id int) (*Usuario, error) {
    if id <= 0 {
        return nil, fmt.Errorf("id invalido: %d", id)
    }
    // buscar en BD...
    return nil, fmt.Errorf("usuario con id %d no encontrado", id)
}

El Patron if err != nil

Es el patron fundamental de manejo de errores en Go:

func leerConfig(ruta string) (*Config, error) {
    data, err := os.ReadFile(ruta)
    if err != nil {
        return nil, fmt.Errorf("leyendo config: %w", err)
    }

    var cfg Config
    err = json.Unmarshal(data, &cfg)
    if err != nil {
        return nil, fmt.Errorf("parseando config: %w", err)
    }

    return &cfg, nil
}

Cada operacion que puede fallar se verifica inmediatamente. El flujo feliz avanza en linea recta.

Error Wrapping con %w

Desde Go 1.13, puedes envolver errores para agregar contexto sin perder el error original:

func abrirDB(dsn string) (*sql.DB, error) {
    db, err := sql.Open("postgres", dsn)
    if err != nil {
        return nil, fmt.Errorf("abrirDB: %w", err)
    }

    err = db.Ping()
    if err != nil {
        return nil, fmt.Errorf("abrirDB ping: %w", err)
    }

    return db, nil
}

El verbo %w (wrap) preserva la cadena de errores. Usa %v si no quieres que sea desenvuelto.

errors.Is y errors.As

errors.Is

Compara contra un error especifico, recorriendo la cadena de wrapping:

func leerArchivo(nombre string) ([]byte, error) {
    data, err := os.ReadFile(nombre)
    if err != nil {
        return nil, fmt.Errorf("leerArchivo %s: %w", nombre, err)
    }
    return data, nil
}

func main() {
    _, err := leerArchivo("noexiste.txt")
    if err != nil {
        // Verifica el error original a traves del wrapping
        if errors.Is(err, os.ErrNotExist) {
            fmt.Println("El archivo no existe")
        } else {
            fmt.Println("Error:", err)
        }
    }
}

errors.As

Extrae un tipo especifico de error de la cadena:

type ErrorValidacion struct {
    Campo   string
    Mensaje string
}

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

func registrar(nombre string) error {
    if nombre == "" {
        return fmt.Errorf("registrar: %w", &ErrorValidacion{
            Campo:   "nombre",
            Mensaje: "es requerido",
        })
    }
    return nil
}

func main() {
    err := registrar("")
    var ve *ErrorValidacion
    if errors.As(err, &ve) {
        fmt.Printf("Error de validacion en campo '%s': %s\n",
            ve.Campo, ve.Mensaje)
    }
}
FuncionPropositoCompara por
errors.Is(err, target)Es este error exacto?Valor/identidad
errors.As(err, &target)Es de este tipo?Tipo

Sentinel Errors

Son errores predefinidos como variables de paquete. Sirven como senales conocidas:

var (
    ErrNoEncontrado = errors.New("recurso no encontrado")
    ErrNoAutorizado = errors.New("no autorizado")
    ErrYaExiste     = errors.New("el recurso ya existe")
)

func obtenerProducto(id string) (*Producto, error) {
    producto, existe := catalogo[id]
    if !existe {
        return nil, ErrNoEncontrado
    }
    return producto, nil
}

func main() {
    _, err := obtenerProducto("xyz")
    if errors.Is(err, ErrNoEncontrado) {
        fmt.Println("Producto no existe, creando uno nuevo...")
    }
}

Sentinel errors comunes en la stdlib:

ErrorPaqueteSignificado
io.EOFioFin de lectura
sql.ErrNoRowsdatabase/sqlQuery sin resultados
os.ErrNotExistosArchivo no existe
os.ErrPermissionosSin permisos
context.CanceledcontextContexto cancelado
context.DeadlineExceededcontextTimeout

Errores Custom

Para errores con informacion estructurada, implementa la interfaz error:

type ErrorHTTP struct {
    Codigo  int
    Mensaje string
    URL     string
}

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

func (e *ErrorHTTP) EsClientError() bool {
    return e.Codigo >= 400 && e.Codigo < 500
}

func llamarAPI(url string) ([]byte, error) {
    resp, err := http.Get(url)
    if err != nil {
        return nil, fmt.Errorf("llamarAPI: %w", err)
    }
    defer resp.Body.Close()

    if resp.StatusCode != http.StatusOK {
        return nil, &ErrorHTTP{
            Codigo:  resp.StatusCode,
            Mensaje: resp.Status,
            URL:     url,
        }
    }

    return io.ReadAll(resp.Body)
}

func main() {
    _, err := llamarAPI("https://api.ejemplo.com/datos")
    if err != nil {
        var httpErr *ErrorHTTP
        if errors.As(err, &httpErr) {
            if httpErr.EsClientError() {
                fmt.Println("Error del cliente:", httpErr)
            } else {
                fmt.Println("Error del servidor:", httpErr)
            }
        } else {
            fmt.Println("Error de red:", err)
        }
    }
}

Errores Multiples con errors.Join (Go 1.20+)

Combina varios errores en uno solo:

func validarFormulario(nombre, email string, edad int) error {
    var errs []error

    if nombre == "" {
        errs = append(errs, errors.New("nombre es requerido"))
    }
    if !strings.Contains(email, "@") {
        errs = append(errs, errors.New("email invalido"))
    }
    if edad < 18 {
        errs = append(errs, fmt.Errorf("edad minima 18, recibido: %d", edad))
    }

    return errors.Join(errs...)
}

func main() {
    err := validarFormulario("", "sinArroba", 15)
    if err != nil {
        fmt.Println("Errores de validacion:")
        fmt.Println(err)
        // nombre es requerido
        // email invalido
        // edad minima 18, recibido: 15
    }

    // errors.Is funciona con cada error individual
    fmt.Println(errors.Is(err, nil)) // false
}

Buenas Practicas

1. Agrega contexto al propagar

// MAL: pierde informacion
if err != nil {
    return err
}

// BIEN: agrega contexto
if err != nil {
    return fmt.Errorf("guardando usuario %s: %w", u.Nombre, err)
}

2. Maneja el error solo una vez

// MAL: log + return (se duplica el manejo)
if err != nil {
    log.Println("error:", err)
    return err
}

// BIEN: o log o return, no ambos
if err != nil {
    return fmt.Errorf("procesando pedido: %w", err)
}

3. No ignores errores

// MAL: error silenciado
json.Unmarshal(data, &config)

// BIEN: siempre verifica
if err := json.Unmarshal(data, &config); err != nil {
    return fmt.Errorf("parseando config: %w", err)
}

4. Usa sentinel errors para flujo de control

for {
    linea, err := lector.ReadString('\n')
    if errors.Is(err, io.EOF) {
        break // fin normal de lectura
    }
    if err != nil {
        return fmt.Errorf("leyendo: %w", err)
    }
    procesar(linea)
}

5. Documenta los errores que retornas

// BuscarUsuario busca un usuario por ID.
// Retorna ErrNoEncontrado si el usuario no existe.
// Retorna ErrNoAutorizado si no hay permisos.
func BuscarUsuario(id string) (*Usuario, error) {
    // ...
}

Resumen

ConceptoDescripcion
error interfaceError() string es todo lo que necesitas
errors.NewError simple sin formato
fmt.Errorf("%w")Error con contexto + wrapping
errors.IsCompara por valor en la cadena
errors.AsExtrae por tipo en la cadena
Sentinel errorsVariables de error predefinidas
errors.JoinCombina multiples errores (Go 1.20+)

← Capitulo 11: Interfaces | Capitulo 13: Goroutines →