Los errores son valores
Una función de Go que puede fallar devuelve un error como último resultado. Quien la llama lo comprueba de inmediato.
Salida:
parsed: 42
could not parse: strconv.Atoi: parsing "forty-two": invalid syntax
Ese es todo el mecanismo. No hay excepciones, ni try ni catch, ni flujo de control oculto: un error solo viaja por donde tu código lo pasa. El coste es la repetición visible de if err != nil. La ventaja es que cada punto de fallo se ve en la página, y en cada uno decides qué pasa.
El tipo error
error es una interfaz integrada con un solo método:
type error interface {
Error() string
}
Cualquier tipo con un método Error() string es un error. Un error nil significa éxito. Imprimir un error con fmt.Println(err) o %v llama a Error().
Crear errores
Dos funciones cubren la mayoría de los casos.
errors.New crea un error con un texto fijo. fmt.Errorf lo formatea, con los mismos verbos que Printf. Por convención, los mensajes de error empiezan en minúscula y no llevan puntuación final, porque suelen quedar incrustados en mensajes más largos: load config: open app.yaml: no such file or directory.
El patrón if err != nil
La forma idiomática es: llamar, comprobar, retornar pronto. La ruta de éxito queda en el margen izquierdo, y cada fallo sale en cuanto ocurre.
func loadUser(id int) (*User, error) {
row, err := db.Query(id)
if err != nil {
return nil, err
}
u, err := parseUser(row)
if err != nil {
return nil, err
}
if err := u.Validate(); err != nil {
return nil, err
}
return u, nil
}
Dos convenciones a tener en cuenta:
- Si hay error, devuelve el valor cero en los demás resultados (
nil,0,""). Quien llama no debe usarlos cuandoerr != nil. if err := f(); err != nillimitaerralifcuando la función solo devuelve un error. Mantiene limpio el ámbito exterior.
Evita el else después de retornar un error. if err != nil { return err } else { ... } solo sangra la ruta feliz sin motivo.
Añadir contexto al devolver un error
Un error que se pasa hacia arriba sin cambios pierde la historia de dónde vino. open config.yaml: no such file or directory no te dice qué paso del arranque falló. Añade contexto con fmt.Errorf y el verbo %w:
Salida:
start server: read config: open /etc/myapp/config.yaml: no such file or directory
true
Cada capa añade lo que estaba haciendo, y el mensaje final se lee como un rastro desde la parte alta de la llamada hasta la causa. Un buen contexto nombra la operación y la entrada: parse line 12, fetch user 42. No añadas "error" o "failed" en cada nivel; el mensaje ya es un error.
%w envuelve: guarda el error original dentro del nuevo, para que errors.Is y errors.As puedan seguir encontrándolo. %v solo copia el texto. Usa %v cuando quieras ocultar a propósito un detalle de implementación, por ejemplo para que quien llama no acabe dependiendo del tipo de error de un driver de base de datos.
Comprobar errores concretos: errors.Is y errors.As
A veces quien llama necesita reaccionar a un tipo de fallo: un archivo que falta significa "usar valores por defecto", un timeout significa "reintentar". Dos funciones responden a eso, y las dos atraviesan todas las capas de envoltura.
Reglas prácticas:
- Compara con valores de error predefinidos (centinelas como
io.EOF,os.ErrNotExist,sql.ErrNoRows) usandoerrors.Is, no==.==falla en cuanto el error está envuelto. - Extrae un error con tipo usando
errors.As, no una aserción de tipo, por la misma razón.errors.Asrecibe un puntero a una variable del tipo de destino. - Nunca compares con el texto de
err.Error(). Los mensajes cambian entre versiones, y cuando cambian, la comparación de texto se rompe en silencio.
Definir tus propios errores centinela y tipos de error, y combinar varios errores con errors.Join, se explica en errores personalizados.
Maneja cada error una vez
Un error debe manejarse exactamente una vez. Manejarlo significa una de estas cosas: devolverlo (normalmente envuelto), registrarlo en el log y continuar, reintentar o convertirlo en una respuesta para el usuario. Hacer dos de ellas es el bug de errores más común en código Go.
// Wrong: logged here, and returned, so it is logged again by every caller.
if err != nil {
log.Printf("could not fetch user: %v", err)
return err
}
// Right: add context and return. The top of the program logs once.
if err != nil {
return fmt.Errorf("fetch user %d: %w", id, err)
}
Registrar y devolver produce el mismo fallo varias veces en los logs, cada una con menos contexto que el mensaje final. Deja que los errores suban hasta el sitio que puede decidir qué hacer (un handler HTTP, un main, un bucle de worker), y registra allí.
Dónde acaban los errores
En la parte alta del programa, algo tiene que actuar sobre el error. En main eso suele significar imprimirlo y salir con un estado distinto de cero:
Ejecutarlo sin argumentos imprime error: usage: app <name> en stderr y sale con estado 1 (escribe un nombre en el panel Args para ver la otra ruta). Mantener main con esta forma, con el trabajo real en run, hace el programa testeable y garantiza que las sentencias defer dentro de run se sigan ejecutando, porque os.Exit se salta las llamadas diferidas.
En un servidor HTTP la parte alta es el handler: traduce el error a un código de estado y a un mensaje seguro para el cliente, y registra el mensaje detallado para ti.
Errores que puedes ignorar y errores que no
Ignorar un error a veces es correcto, pero hazlo explícito con _ para que quien lee sepa que fue una decisión:
_ = conn.SetDeadline(t) // best effort
Algunas llamadas no pueden fallar en la práctica (strings.Builder.WriteString, bytes.Buffer.Write). Otras parecen inofensivas y no lo son: Close sobre un archivo en el que escribiste puede informar de que los datos nunca llegaron al disco, y json.Marshal falla con channels y funciones. Ante la duda, comprueba.
El linter errcheck (incluido en golangci-lint) informa de los errores sin comprobar. go vet no los marca por sí solo.
Errores y panics
Go también tiene panic, pero no es un sistema de excepciones. Usa errores para todo lo que puede salir mal durante el funcionamiento normal: entrada incorrecta, archivos que faltan, fallos de red. Reserva panic para bugs (un estado imposible, un invariante roto) y para fallos de arranque en los que continuar no tiene sentido. Una librería casi nunca debería provocar un panic a través de su API. Consulta panic y recover.
Reducir la repetición
if err != nil es verboso, y las propuestas para añadirle sintaxis nueva se han rechazado una y otra vez; el equipo de Go anunció en 2025 que ya no busca cambios de sintaxis para el manejo de errores. Algunos patrones reducen el ruido dentro del lenguaje:
- Retorna pronto y mantén las funciones pequeñas. La mayor parte de la repetición viene de funciones largas que hacen muchos pasos.
- El error pegajoso. Para una secuencia de escrituras, guarda el primer error en un campo de un struct y haz que las llamadas posteriores no hagan nada una vez que está definido.
bufio.Writerfunciona así: compruebas el error una sola vez después deFlush. - Envuelve una vez por función. Una closure diferida sobre un resultado con nombre puede añadir el mismo contexto a cada error que devuelve la función (consulta defer).
Errores comunes
- Usar un valor cuando err no es nil. Primero comprueba, luego usa.
- Registrar y devolver. Elige una cosa.
- Comparar con
==después de envolver. Usaerrors.Is. - Perder la causa con
%v. Usa%wsalvo que ocultarla sea justo lo que buscas. - Devolver un puntero nil con tipo como
error.var e *MyErr; return eno es nil para quien llama. Devuelve unnilliteral. - Mensajes con mayúscula inicial o puntuación.
errors.New("Failed to connect.")queda mal una vez envuelto. Escribeconnect to db: ....
Preguntas frecuentes
¿Cómo funciona el manejo de errores en Go?
Las funciones que pueden fallar devuelven un error como último resultado. Quien llama lo comprueba de inmediato: v, err := f(); if err != nil { return err }. Un error es un valor de interfaz normal con un método, Error() string, y nil significa éxito. No hay excepciones.
¿Tiene Go try/catch?
No. Go no tiene excepciones ni try/catch. Los fallos esperados se devuelven como valores error y se comprueban con if err != nil. Existen panic y recover, pero son para bugs de programación y estados irrecuperables, no para el flujo normal de errores.
¿Cómo devuelvo un error en Go?
Declara error como último resultado y devuelve nil si todo va bien. Crea los errores con errors.New("message") para un texto fijo o con fmt.Errorf("reading %s: %w", name, err) para añadir contexto a un error que recibiste. Si algo falla, devuelve valores cero en los demás resultados.
¿Qué diferencia hay entre %w y %v en fmt.Errorf?
Los dos meten el mensaje del error original en el nuevo. %w además lo envuelve, así que errors.Is y errors.As pueden seguir encontrando el original. %v produce un error nuevo que solo tiene el texto. Usa %w cuando quien llama pueda necesitar comprobar la causa, y %v cuando quieras ocultarla.
¿Cómo compruebo qué error se ha devuelto en Go?
Usa errors.Is(err, target) para comparar con un error centinela como io.EOF u os.ErrNotExist, y errors.As(err, &target) para extraer un tipo de error concreto como *fs.PathError. Los dos recorren los errores envueltos. Evita comparar los strings de err.Error().