Skip to main content

Command Palette

Search for a command to run...

Manejo de Errores en Go: Errores como Valores, no Excepciones

Updated
10 min read

En los posts anteriores exploramos structs, interfaces, métodos y collections en Go. Hoy abordaremos uno de los aspectos más distintivos del lenguaje: el manejo de errores. Si vienes de Java, donde usas try-catch y excepciones, Go tiene un enfoque completamente diferente que al principio puede parecer verboso, pero que en realidad te da más control y claridad.

Filosofía: Errores como Valores

En Go, los errores son valores, no excepciones. No hay try-catch. En su lugar, las funciones retornan errores como parte de su valor de retorno:

// Convención: error siempre es el último valor de retorno
result, err := doSomething()
if err != nil {
    // Manejar el error
    return err
}
// Continuar con el resultado

Esta filosofía tiene ventajas importantes:

  • Explícito: Ves claramente qué funciones pueden fallar

  • Control: Decides cómo manejar cada error

  • Performance: No hay overhead de stack unwinding como en excepciones

  • Claridad: El flujo de código es más predecible

Crear Errores Básicos

Errores Simples con errors.New()

var (
    ErrNotFound     = errors.New("resource not found")
    ErrInvalidInput = errors.New("invalid input")
    ErrUnauthorized = errors.New("unauthorized")
)

Estos son sentinel errors - errores predefinidos que puedes comparar directamente. Son útiles para errores conocidos que el llamador puede manejar específicamente.

Errores con Formato con fmt.Errorf()

func createFormattedError(id int) error {
    return fmt.Errorf("user with id %d not found", id)
}

fmt.Errorf() es útil cuando necesitas incluir información dinámica en el mensaje de error.

Errores Personalizados

Puedes crear tipos de error personalizados para incluir más información:

type ValidationError struct {
    Field   string
    Message string
}

// Implementar la interfaz error
func (e *ValidationError) Error() string {
    return fmt.Sprintf("validation error on field '%s': %s", e.Field, e.Message)
}

Esto te permite:

  • Agregar campos adicionales al error

  • Extraer información específica más tarde

  • Crear jerarquías de errores

Wrapping Errors: Agregar Contexto

Una de las características más poderosas de Go (desde Go 1.13) es el wrapping de errores. Te permite agregar contexto a un error sin perder el error original.

El Problema sin Wrapping

// ❌ Sin contexto
func readFile(filename string) error {
    file, err := os.Open(filename)
    if err != nil {
        return err  // Solo retorna el error original sin contexto
    }
    // ...
}

Si este error ocurre en una función profunda, no sabes dónde ni por qué falló.

La Solución: Wrapping con %w

func readFile(filename string) error {
    file, err := os.Open(filename)
    if err != nil {
        // Wrap el error agregando contexto
        return fmt.Errorf("failed to open file %s: %w", filename, err)
    }
    defer file.Close()

    data := make([]byte, 100)
    _, err = file.Read(data)
    if err != nil && err != io.EOF {
        // Wrap el error agregando contexto
        return fmt.Errorf("failed to read file %s: %w", filename, err)
    }

    return nil
}

El verbo %w en fmt.Errorf() crea un error que envuelve el error original. Esto te permite:

  • Agregar contexto útil en cada capa

  • Mantener el error original para verificación

  • Crear cadenas de errores informativas

Verificar Errores Específicos: errors.Is()

errors.Is() verifica si un error es o contiene un error específico en su cadena de wrapping:

err := readFile("nonexistent.txt")

// Verificar si el error es específicamente os.ErrNotExist
if errors.Is(err, os.ErrNotExist) {
    fmt.Println("File does not exist")
}

// Verificar con nuestro error personalizado
if errors.Is(err, ErrNotFound) {
    fmt.Println("Resource not found")
}

Importante: errors.Is() funciona incluso si el error está envuelto:

// Esto funciona aunque ErrNotFound esté envuelto
if errors.Is(err, ErrNotFound) {
    // Manejar error específico
}

Ejemplo Práctico: Manejo de Errores Específicos

func (r *UserRepository) GetUser(id int) (*User, error) {
    user, exists := r.users[id]
    if !exists {
        return nil, fmt.Errorf("failed to get user: %w", ErrUserNotFound)
    }
    return user, nil
}

// En el llamador
user, err := repo.GetUser(123)
if err != nil {
    if errors.Is(err, ErrUserNotFound) {
        // Manejar específicamente el caso de usuario no encontrado
        return fmt.Errorf("user %d not found", 123)
    }
    // Otros errores
    return err
}

Extraer Errores por Tipo: errors.As()

errors.As() es similar a instanceof en Java, pero para errores. Te permite extraer un error de un tipo específico de la cadena de errores:

err := &ValidationError{
    Field:   "email",
    Message: "invalid format",
}

var validationErr *ValidationError
if errors.As(err, &validationErr) {
    fmt.Printf("Validation error on field: %s\n", validationErr.Field)
    fmt.Printf("Message: %s\n", validationErr.Message)
}

Nota: El segundo parámetro debe ser un puntero a un puntero del tipo de error.

Cuándo Usar errors.Is() vs errors.As()

SituaciónUsar
Comparar con error predefinido (sentinel)errors.Is()
Extraer información de error personalizadoerrors.As()
Verificar tipo de errorerrors.As()

Combinar Múltiples Errores: errors.Join()

errors.Join() (desde Go 1.20) combina múltiples errores en uno solo. Es perfecto para validaciones que pueden tener múltiples errores:

func validateUser(name, email string) error {
    var errs []error

    if name == "" {
        errs = append(errs, &ValidationError{
            Field:   "name",
            Message: "cannot be empty",
        })
    }
    if email == "" {
        errs = append(errs, &ValidationError{
            Field:   "email",
            Message: "cannot be empty",
        })
    }
    if len(email) > 0 && !strings.Contains(email, "@") {
        errs = append(errs, &ValidationError{
            Field:   "email",
            Message: "invalid format",
        })
    }

    if len(errs) > 0 {
        return errors.Join(errs...)
    }
    return nil
}

Para extraer los errores individuales:

if err := validateUser("", ""); err != nil {
    if joinedErr, ok := err.(interface{ Unwrap() []error }); ok {
        for _, e := range joinedErr.Unwrap() {
            fmt.Printf("  - %v\n", e)
        }
    }
}

Patrón: Sentinel Errors

Los sentinel errors son errores predefinidos que representan condiciones específicas y conocidas:

var (
    ErrUserNotFound    = errors.New("user not found")
    ErrInvalidPassword = errors.New("invalid password")
    ErrUserExists      = errors.New("user already exists")
)

Ventajas:

  • Comparación directa con errors.Is()

  • Documentación clara de errores posibles

  • Manejo específico por tipo de error

Ejemplo de uso:

func (r *UserRepository) CreateUser(id int, username, email string) error {
    if _, exists := r.users[id]; exists {
        return ErrUserExists  // Retornar sentinel error
    }
    r.users[id] = &User{ID: id, Username: username, Email: email}
    return nil
}

// En el llamador
if err := repo.CreateUser(1, "john", "john@example.com"); err != nil {
    if errors.Is(err, ErrUserExists) {
        // Manejar específicamente el caso de usuario existente
        fmt.Println("User already exists, continuing...")
    } else {
        return fmt.Errorf("failed to create user: %w", err)
    }
}

Manejo de Errores en Capas

En aplicaciones reales, los errores pasan por múltiples capas. Es importante agregar contexto en cada capa mientras preservas el error original:

// Capa de repositorio
func (r *UserRepository) GetUser(id int) (*User, error) {
    user, exists := r.users[id]
    if !exists {
        return nil, fmt.Errorf("failed to get user: %w", ErrUserNotFound)
    }
    return user, nil
}

// Capa de servicio
func (s *UserService) Login(username, password string) (*User, error) {
    user, err := s.repo.Authenticate(username, password)
    if err != nil {
        if errors.Is(err, ErrUserNotFound) {
            return nil, fmt.Errorf("login failed: user not found")
        }
        if errors.Is(err, ErrInvalidPassword) {
            return nil, fmt.Errorf("login failed: invalid password")
        }
        return nil, fmt.Errorf("login failed: %w", err)
    }
    return user, nil
}

Principios:

  1. Wrap en cada capa con contexto relevante

  2. Verifica errores específicos con errors.Is() cuando sea apropiado

  3. No ignores errores - siempre maneja o propaga

  4. Agrega contexto útil - qué operación falló y dónde

Comparación: Java vs Go

AspectoJavaGo
MecanismoExcepciones (throw/catch)Valores de retorno
VisibilidadImplícita (cualquier función puede lanzar)Explícita (error en firma)
PerformanceOverhead de stack unwindingSin overhead
Control de flujoPuede saltar múltiples nivelesFlujo explícito
ContextoStack trace automáticoDebes agregarlo manualmente
Verificacióninstanceoferrors.Is() / errors.As()

Ejemplo Comparativo

Java:

try {
    User user = repository.getUser(id);
    // ...
} catch (UserNotFoundException e) {
    // Manejar error
} catch (Exception e) {
    // Manejar otros errores
}

Go:

user, err := repository.GetUser(id)
if err != nil {
    if errors.Is(err, ErrUserNotFound) {
        // Manejar error específico
        return fmt.Errorf("user %d not found", id)
    }
    // Manejar otros errores
    return fmt.Errorf("failed to get user: %w", err)
}
// Continuar con user

Mejores Prácticas

1. Siempre Verifica Errores

// ❌ MAL - Ignorar error
result, _ := doSomething()

// ✅ BIEN - Verificar error
result, err := doSomething()
if err != nil {
    return err
}

2. Agrega Contexto con Wrapping

// ❌ MAL - Sin contexto
if err != nil {
    return err
}

// ✅ BIEN - Con contexto
if err != nil {
    return fmt.Errorf("failed to process user %d: %w", userID, err)
}

3. Usa Sentinel Errors para Errores Conocidos

// ✅ Definir errores predefinidos
var ErrUserNotFound = errors.New("user not found")

// ✅ Retornar sentinel error
if !exists {
    return ErrUserNotFound
}

// ✅ Verificar con errors.Is()
if errors.Is(err, ErrUserNotFound) {
    // Manejar específicamente
}

4. No Envuelvas Errores Múltiples Veces Sin Razón

// ❌ MAL - Envolver sin agregar valor
if err != nil {
    return fmt.Errorf("error: %w", err)  // No agrega contexto útil
}

// ✅ BIEN - Agregar contexto útil
if err != nil {
    return fmt.Errorf("failed to save user %s: %w", username, err)
}

5. Usa errors.Join() para Múltiples Errores

// ✅ Para validaciones con múltiples errores
var errs []error
if name == "" {
    errs = append(errs, ErrNameRequired)
}
if email == "" {
    errs = append(errs, ErrEmailRequired)
}
if len(errs) > 0 {
    return errors.Join(errs...)
}

Errores Comunes

❌ Error 1: Ignorar Errores

// ❌ MAL
result, _ := doSomething()  // Ignora el error

// ✅ BIEN
result, err := doSomething()
if err != nil {
    return err
}

❌ Error 2: No Agregar Contexto

// ❌ MAL
if err != nil {
    return err  // Sin contexto
}

// ✅ BIEN
if err != nil {
    return fmt.Errorf("failed to process: %w", err)
}

❌ Error 3: Comparar Errores Envueltos con ==

// ❌ MAL - No funciona con errores envueltos
if err == ErrNotFound {
    // ...
}

// ✅ BIEN - Usa errors.Is()
if errors.Is(err, ErrNotFound) {
    // ...
}

Ejemplo Completo: Sistema de Autenticación

type UserService struct {
    repo *UserRepository
}

func (s *UserService) RegisterUser(id int, username, email, password string) error {
    // Validar entrada
    if err := validateUser(username, email); err != nil {
        return fmt.Errorf("validation failed: %w", err)
    }

    // Crear usuario
    if err := s.repo.CreateUser(id, username, email); err != nil {
        if errors.Is(err, ErrUserExists) {
            return fmt.Errorf("registration failed: user already exists")
        }
        return fmt.Errorf("registration failed: %w", err)
    }

    return nil
}

func (s *UserService) Login(username, password string) (*User, error) {
    user, err := s.repo.Authenticate(username, password)
    if err != nil {
        if errors.Is(err, ErrUserNotFound) {
            return nil, fmt.Errorf("login failed: user not found")
        }
        if errors.Is(err, ErrInvalidPassword) {
            return nil, fmt.Errorf("login failed: invalid password")
        }
        return nil, fmt.Errorf("login failed: %w", err)
    }
    return user, nil
}

Este ejemplo muestra:

  • Validación con errors.Join()

  • Manejo específico de sentinel errors

  • Agregar contexto en cada capa

  • Preservar el error original para verificación

Conclusiones

El manejo de errores en Go es diferente pero poderoso:

Errores como valores: Más explícito y controlable que excepciones

Wrapping: Agrega contexto sin perder el error original

errors.Is(): Verifica errores específicos incluso cuando están envueltos

errors.As(): Extrae información de errores personalizados

errors.Join(): Combina múltiples errores para validaciones

Conceptos clave a recordar:

  • Siempre verifica errores explícitamente

  • Agrega contexto con fmt.Errorf() y %w

  • Usa sentinel errors para errores conocidos

  • Verifica con errors.Is(), no con ==

  • Extrae información con errors.As() para errores personalizados

Aunque puede parecer verboso al principio, este enfoque te da más control y hace que el código sea más predecible y fácil de depurar.

Próximos Pasos

En el siguiente post exploraremos los paquetes importantes de Go: context, io, fmt, sync, y reflect. Estos paquetes son fundamentales para escribir código Go profesional y entenderás cómo se integran con todo lo que hemos aprendido hasta ahora.


¿Tienes preguntas sobre el manejo de errores en Go? Déjame saber en los comentarios. Y si quieres ver el código completo de estos ejemplos, puedes encontrarlo en mi repositorio go-mastery-lab.

Enjoy!

José Díaz

+51 939965148

More from this blog

JoeDayz

53 posts

Community Guy | Java Champion | AWS Architect | Software Architect