Especificación de Manejo de Errores en Golang

Tipos de Errores

Hay algunas opciones para declarar errores. Antes de elegir la opción que mejor se adapte a su caso de uso, considere lo siguiente:

  • ¿El llamante necesita hacer coincidir el error para manejarlo? Si es así, debemos admitir las funciones errors.Is o errors.As declarando variables de error de nivel superior o tipos personalizados.
  • ¿El mensaje de error es una cadena estática o una cadena dinámica que requiere información contextual? Para cadenas estáticas, podemos usar errors.New, pero para este último, debemos usar fmt.Errorf o un tipo de error personalizado.
  • ¿Estamos pasando nuevos errores devueltos por funciones secundarias? Si es así, consulte la sección de envoltura de errores.
¿Coincidencia de Error? Mensaje de Error Guía
No estática errors.New
No dinámica fmt.Errorf
estática var de nivel superior con errors.New
dinámica tipo de error personalizado

Por ejemplo, use errors.New para representar errores con cadenas estáticas. Si el llamante necesita hacer coincidir y manejar este error, expórtelo como una variable para admitir la coincidencia con errors.Is.

Sin Coincidencia de Errores

// paquete foo

func Abrir() error {
  return errors.New("no se pudo abrir")
}

// paquete bar

if err := foo.Abrir(); err != nil {
  // No se puede manejar el error.
  panic("error desconocido")
}

Coincidencia de Errores

// paquete foo

var ErrNoSePudoAbrir = errors.New("no se pudo abrir")

func Abrir() error {
  return ErrNoSePudoAbrir
}

// paquete bar

if err := foo.Abrir(); err != nil {
  if errors.Is(err, foo.ErrNoSePudoAbrir) {
    // manejar el error
  } else {
    panic("error desconocido")
  }
}

Para errores con cadenas dinámicas, use fmt.Errorf si el llamante no necesita hacer coincidir. Si de hecho el mismo necesita hacer coincidir, entonces use un error personalizado.

Sin Coincidencia de Errores

// paquete foo

func Abrir(archivo string) error {
  return fmt.Errorf("archivo %q no encontrado", archivo)
}

// paquete bar

if err := foo.Abrir("archivo.txt"); err != nil {
  // No se puede manejar el error.
  panic("error desconocido")
}

Coincidencia de Errores

// paquete foo

type ErrorNoEncontrado struct {
  Archivo string
}

func (e *ErrorNoEncontrado) Error() string {
  return fmt.Sprintf("archivo %q no encontrado", e.Archivo)
}

func Abrir(archivo string) error {
  return &ErrorNoEncontrado{Archivo: archivo}
}

// paquete bar

if err := foo.Abrir("archivo.txt"); err != nil {
  var noEncontrado *ErrorNoEncontrado
  if errors.As(err, &noEncontrado) {
    // manejar el error
  } else {
    panic("error desconocido")
  }
}

Tenga en cuenta que si exporta variables o tipos de error desde un paquete, estos formarán parte de la API pública del paquete.

Envolver errores

Cuando ocurre un error al llamar a otro método, generalmente hay tres maneras de manejarlo:

  • Devolver el error original tal como está.
  • Utilizar fmt.Errorf con %w para agregar contexto al error y luego devolverlo.
  • Utilizar fmt.Errorf con %v para agregar contexto al error y luego devolverlo.

Si no hay contexto adicional para agregar, devolver el error original tal como está. Esto preservará el tipo y el mensaje de error original. Esto es particularmente adecuado cuando el mensaje de error subyacente contiene suficiente información para rastrear de dónde provino el error.

De lo contrario, agregue contexto al mensaje de error tanto como sea posible para que no ocurran errores ambiguos como "conexión rechazada". En su lugar, recibirás errores más útiles, como "llamando al servicio foo: conexión rechazada".

Utiliza fmt.Errorf para agregar contexto a tus errores y elige entre los verbos %w o %v según si el llamante debe poder igualar y extraer la causa raíz.

  • Utiliza %w si el llamante debe tener acceso al error subyacente. Este es un buen valor por defecto para la mayoría de errores de envoltura, pero ten en cuenta que el llamante puede empezar a depender de este comportamiento. Por lo tanto, para errores de envoltura que son variables o tipos conocidos, regístralos y pruébalos como parte del contrato de la función.
  • Utiliza %v para ocultar el error subyacente. El llamante no podrá igualarlo, pero puedes cambiar a %w en el futuro si es necesario.

Al agregar contexto al error devuelto, evita usar frases como "falló al" para mantener el contexto conciso. Cuando el error se propague a través de la pila, se irá apilando capa por capa:

No recomendado:

s, err := store.New()
if err != nil {
    return fmt.Errorf(
        "falló al crear una nueva tienda: %w", err)
}

// falló al x: falló al y: falló al crear una nueva tienda: el error

Recomendado:

s, err := store.New()
if err != nil {
    return fmt.Errorf(
        "nueva tienda: %w", err)
}
// x: y: nueva tienda: el error

Sin embargo, una vez que el error se envía a otro sistema, debe quedar claro que el mensaje es un error (por ejemplo, una etiqueta "err" o un prefijo "Fallido" en los registros).

Nomenclatura incorrecta

Para valores de error almacenados como variables globales, utiliza el prefijo Err o err según si están exportados. Consulta las pautas. Para constantes y variables de nivel superior no exportadas, utiliza un guion bajo (_) como prefijo.

var (
  // Exporta los dos siguientes errores para que los usuarios de este paquete puedan igualarlos con errors.Is.
  ErrEnlaceRoto = errors.New("el enlace está roto")
  ErrNoSePudoAbrir = errors.New("no se pudo abrir")

  // Este error no se exporta porque no queremos que forme parte de nuestra API pública. Aun así, aún podemos usarlo dentro del paquete con errors.
  errNoEncontrado = errors.New("no encontrado")
)

Para tipos de error personalizados, utiliza el sufijo Error.

// Del mismo modo, este error se exporta para que los usuarios de este paquete puedan igualarlo con errors.As.
type ErrorNoEncontrado struct {
  Archivo string
}

func (e *ErrorNoEncontrado) Error() string {
  return fmt.Sprintf("archivo %q no encontrado", e.Archivo)
}

// Este error no se exporta porque no queremos que forme parte de la API pública. Aun así, aún podemos usarlo dentro de un paquete con errors.As.
type errorResolucion struct {
  Ruta string
}

func (e *errorResolucion) Error() string {
  return fmt.Sprintf("resolución %q", e.Ruta)
}

Manejo de Errores

Cuando el llamante recibe un error del llamado, puede manejar el error de diversas maneras según su comprensión del error.

Esto incluye, pero no se limita a:

  • Coincidir el error con errors.Is o errors.As si el llamado ha acordado una definición específica de error, y manejar la ramificación de diferentes maneras
  • Registrar el error y degradar graciosamente si el error es recuperable
  • Devolver un error bien definido si representa una condición de fallo específica del dominio
  • Devolver el error, ya sea envuelto o textual

Independientemente de cómo el llamante maneje el error, típicamente debería manejar cada error solo una vez. Por ejemplo, el llamante no debería registrar el error y luego devolverlo, ya que su llamante podría también manejar el error.

Por ejemplo, considera los siguientes escenarios:

Malo: Registrar el error y devolverlo

Otros llamantes más arriba en la pila pueden tomar acciones similares sobre este error. Esto crearía mucho ruido en los registros de la aplicación con poco beneficio.

u, err := getUser(id)
if err != nil {
  // MALO: Ver descripción
  log.Printf("No se pudo obtener el usuario %q: %v", id, err)
  return err
}

Bueno: Envolver el error y devolverlo

Los errores más arriba en la pila manejarán este error. Usar %w asegura que puedan coincidir el error con errors.Is o errors.As si es relevante.

u, err := getUser(id)
if err != nil {
  return fmt.Errorf("obtener usuario %q: %w", id, err)
}

Bueno: Registrar el error y degradar graciosamente

Si la operación no es absolutamente necesaria, podemos proporcionar degradación grácil al recuperarnos de ella sin interrumpir la experiencia.

if err := emitMetrics(); err != nil {
  // La falla al escribir las métricas no debería
  // romper la aplicación.
  log.Printf("No se pudieron emitir las métricas: %v", err)
}

Bueno: Coincidir el error y degradar graciosamente de manera apropiada

Si el llamado ha definido un error específico en su acuerdo y el fallo es recuperable, coincidir ese caso de error y degradarse graciosamente. Para todos los otros casos, envolver el error y devolverlo. Los errores más arriba en la pila manejarán otros errores.

tz, err := getUserTimeZone(id)
if err != nil {
  if errors.Is(err, ErrUserNotFound) {
    // El usuario no existe. Usar UTC.
    tz = time.UTC
  } else {
    return fmt.Errorf("obtener usuario %q: %w", id, err)
  }
}

Manejo de Fallas de Asertos

Las afirmaciones de tipo entrarán en pánico con un único valor de retorno en caso de una detección de tipo incorrecta. Por lo tanto, siempre utiliza el "coma, ok" idiomático.

No Recomendado:

t := i.(string)

Recomendado:

t, ok := i.(string)
if !ok {
  // Manejar el error graciosamente
}

Evite usar panic

El código que se ejecuta en el entorno de producción debe evitar usar panic. El panic es la principal fuente de fallas en cascada. Si ocurre un error, la función debe devolver el error y permitir que el llamante decida cómo manejarlo.

No recomendado:

func run(args []string) {
  if len(args) == 0 {
    panic("se requiere un argumento")
  }
  // ...
}

func main() {
  run(os.Args[1:])
}

Recomendado:

func run(args []string) error {
  if len(args) == 0 {
    return errors.New("se requiere un argumento")
  }
  // ...
  return nil
}

func main() {
  if err := run(os.Args[1:]); err != nil {
    fmt.Fprintln(os.Stderr, err)
    os.Exit(1)
  }
}

El panic/recover no es una estrategia de manejo de errores. Solo debe haber un panic cuando ocurre un evento irreparable (por ejemplo, una referencia nula). Una excepción a esto es durante la inicialización del programa: las situaciones que provocarían un panic en el programa deben manejarse durante el inicio del programa.

var _statusTemplate = template.Must(template.New("nombre").Parse("_statusHTML"))

Incluso en el código de prueba, es preferible usar t.Fatal o t.FailNow en lugar del panic para asegurarse de que las fallas estén marcadas.

No recomendado:

// func TestFoo(t *testing.T)

f, err := os.CreateTemp("", "test")
if err != nil {
  panic("error al configurar la prueba")
}

Recomendado:

// func TestFoo(t *testing.T)

f, err := os.CreateTemp("", "test")
if err != nil {
  t.Fatal("error al configurar la prueba")
}