CAP 07 · LEC 02·Manejo de errores

`errors.Is`, `errors.As` y wrapping con `%w`

Wrappear un error es añadirle contexto sin perder el original. Go 1.13 introdujo `%w`, `errors.Is` y `errors.As` para construir cadenas de errores que después puedes inspeccionar.

● INTERMEDIO7 min lecturapor Fernando Herrera · actualizado mayo de 2026
¿Encontraste un error o algo que mejorar?Editá esta lección en GitHub →

Wrappear con `%w`

fmt.Errorf con el verbo %w crea un error nuevo que envuelve al original. El error externo añade contexto; el interno queda accesible para inspección posterior. Es diferente de %s y %v, que solo concatenan el texto y pierden el error original.

package main import ( "errors" "fmt" ) var ErrNotFound = errors.New("not found") func fetchUser(id int) error { // El error original return ErrNotFound } func loadProfile(id int) error { if err := fetchUser(id); err != nil { // %w preserva el error original dentro del wrap return fmt.Errorf("loadProfile(%d): %w", id, err) } return nil } func main() { err := loadProfile(42) fmt.Println(err) // loadProfile(42): not found // Diferencia con %v: el texto se ve igual, // pero el error original NO se puede recuperar. badWrap := fmt.Errorf("loadProfile(%d): %v", 42, ErrNotFound) fmt.Println(badWrap) fmt.Println("recuperable con %w?:", errors.Is(err, ErrNotFound)) fmt.Println("recuperable con %v?:", errors.Is(badWrap, ErrNotFound)) }
SalidaloadProfile(42): not found loadProfile(42): not found recuperable con %w?: true recuperable con %v?: false

`errors.Is` — comparar contra un valor sentinel

errors.Is(err, target) recorre la cadena de wraps y devuelve true si en algún nivel encuentra target. Sirve para comparar contra valores de error (sentinel errors), no contra tipos.

package main import ( "errors" "fmt" ) var ( ErrNotFound = errors.New("not found") ErrUnauthorized = errors.New("unauthorized") ) func readConfig(name string) error { return fmt.Errorf("readConfig(%q): %w", name, ErrNotFound) } func startServer() error { if err := readConfig("server.yaml"); err != nil { return fmt.Errorf("startServer: %w", err) } return nil } func main() { err := startServer() fmt.Println("err:", err) // err: startServer: readConfig("server.yaml"): not found // errors.Is busca a través de toda la cadena de wraps if errors.Is(err, ErrNotFound) { fmt.Println("→ es un error de no encontrado, lo manejamos") } if errors.Is(err, ErrUnauthorized) { fmt.Println("→ NO se imprime, no es de autorización") } }
Salidaerr: startServer: readConfig("server.yaml"): not found → es un error de no encontrado, lo manejamos

`errors.As` — extraer un tipo concreto

errors.As(err, &target) recorre la cadena buscando un error que coincida con el tipo del puntero que le pases. Si lo encuentra, asigna ese error a target y retorna true. Sirve para acceder a campos del error original.

package main import ( "errors" "fmt" ) // Error con datos estructurados type ValidationError struct { Field string Message string } func (e *ValidationError) Error() string { return fmt.Sprintf("validation: %s: %s", e.Field, e.Message) } func validateAge(age int) error { if age < 0 { return &ValidationError{Field: "age", Message: "no puede ser negativa"} } return nil } func saveUser(age int) error { if err := validateAge(age); err != nil { return fmt.Errorf("saveUser: %w", err) } return nil } func main() { err := saveUser(-1) fmt.Println("err:", err) var ve *ValidationError if errors.As(err, &ve) { // Accedemos a los campos del error original fmt.Println("campo inválido:", ve.Field) fmt.Println("mensaje:", ve.Message) } }
Salidaerr: saveUser: validation: age: no puede ser negativa campo inválido: age mensaje: no puede ser negativa
`Is` vs `As`

errors.Is compara contra un valor (un sentinel concreto). errors.As extrae un tipo y te lo asigna para que accedas a sus campos. Usa Is para “¿es este error?” y As para “dame el error como *MyError”.

Cadenas de wrapping

Cada capa de la aplicación puede añadir contexto. El error final cuenta una pequeña historia desde el origen hasta el punto donde se reportó. errors.Is y errors.As siguen funcionando a cualquier profundidad.

package main import ( "errors" "fmt" ) var ErrDBClosed = errors.New("db: connection closed") func queryDB() error { return ErrDBClosed } func getUser(id int) error { if err := queryDB(); err != nil { return fmt.Errorf("getUser(%d): %w", id, err) } return nil } func handleRequest() error { if err := getUser(7); err != nil { return fmt.Errorf("handleRequest: %w", err) } return nil } func main() { err := handleRequest() fmt.Println(err) // handleRequest: getUser(7): db: connection closed // errors.Is sigue toda la cadena fmt.Println("es ErrDBClosed?", errors.Is(err, ErrDBClosed)) }
SalidahandleRequest: getUser(7): db: connection closed es ErrDBClosed? true

`errors.Unwrap` — pelar una capa

errors.Unwrap(err) retorna el error envuelto inmediatamente debajo. Normalmente no lo necesitas en código de aplicación (usa Is/As); es útil cuando implementas un tipo de error custom o quieres recorrer la cadena manualmente.

package main import ( "errors" "fmt" ) var ErrBase = errors.New("base error") func main() { wrapped := fmt.Errorf("nivel 1: %w", ErrBase) deeper := fmt.Errorf("nivel 2: %w", wrapped) fmt.Println("error completo:", deeper) fmt.Println("unwrap 1:", errors.Unwrap(deeper)) fmt.Println("unwrap 2:", errors.Unwrap(errors.Unwrap(deeper))) fmt.Println("unwrap 3:", errors.Unwrap(errors.Unwrap(errors.Unwrap(deeper)))) }
Salidaerror completo: nivel 2: nivel 1: base error unwrap 1: nivel 1: base error unwrap 2: base error unwrap 3: <nil>
No wrappees todo

Wrappear es para añadir contexto útil. Si el caller ya tiene toda la información (por ejemplo dentro de un loop con un índice obvio), un wrap más solo añade ruido. Y si el error es interno de un paquete, a veces conviene no propagarlo (devuelve un error tuyo y oculta el detalle).