Errores en Go

Lección 5 de 8. Go no tiene excepciones. Un error es un valor que se devuelve como cualquier otro, y comprobarlo es tu trabajo, línea a línea. Suena tedioso y lo es un poco; a cambio, leyendo una función sabes exactamente por dónde puede fallar.

El error es un valor

error es una interfaz de la biblioteca estándar con un solo método. Esto es toda su definición:

type error interface {
	Error() string
}

Cualquier cosa con un método Error() string es un error. Por eso una función que puede fallar devuelve dos valores: el resultado y un error que vale nil cuando todo fue bien.

func comprar(unidades int, precio float64) error {
	if unidades <= 0 {
		return ErrSinStock
	}
	if precio < 0 {
		return &ErrPrecioInvalido{Valor: precio}
	}
	return nil
}

El patrón que vas a escribir mil veces

if err := comprar(0, 10); err != nil {
	return err      // o registrarlo, o devolverlo con contexto
}
// a partir de aquí se da por hecho que todo fue bien

Tres cosas de esa forma. Primera: el if con inicialización que vimos en la lección 3, que deja err acotada. Segunda: el camino de error sale pronto, así que el flujo normal se queda sin anidar. Y tercera, la que más cuesta aceptar: no se ignora nunca. Si no te importa, hay que decirlo en voz alta con _ = f(), y eso se ve al leer.

Dos maneras de crear un error

var ErrSinStock = errors.New("sin stock")              // fijo, comparable

return fmt.Errorf("no se puede dividir %v entre cero", a)  // con datos dentro

El primero se llama error centinela: se declara una vez como variable de paquete y quien lo recibe puede compararlo. El segundo se crea al vuelo con los datos del momento.

Envolver: añadir contexto sin perder el original

Un error que sube por cinco funciones y solo dice «sin stock» no ayuda a nadie. Pero reemplazarlo por otro pierde la información de abajo. La solución es envolverlo, con el verbo %w:

func procesarPedido(unidades int, precio float64) error {
	if err := comprar(unidades, precio); err != nil {
		return fmt.Errorf("procesando el pedido: %w", err)
	}
	return nil
}
Cómo se encadenan los errores en Go con %wEnvolver un error con %w conserva el de dentroprocesando el pedido:fmt.Errorf("...: %w", err)sin stockerrors.New("sin stock")envuelve aQué te da cada cosaerr.Error()el texto entero, las dos capaserrors.Is¿hay un ErrSinStock ahí dentro?errors.Assácame el error concreto y sus datoserrors.Unwrapla capa de debajo, una solaSin el %w el error de dentro se pierde y errors.Is deja de encontrarlo. Con %v solo pegas textoLa cadena puede tener tantas capas como funciones haya atravesado

Ahora el texto lleva las dos capas y, además, la de dentro sigue ahí para poder preguntarle:

err := procesarPedido(0, 10)
fmt.Println(err)
fmt.Println("¿es ErrSinStock?", errors.Is(err, ErrSinStock))
salidaprocesando el pedido: sin stock
¿es ErrSinStock? true
%w o %v, y la diferencia importa. Con %v el error de dentro se convierte en texto y se pierde: el mensaje se ve igual, pero errors.Is devuelve false y ya no hay manera de saber qué pasó. Usa %w siempre que quieras que el de arriba pueda seguir preguntando.

errors.Is y errors.As

Son las dos formas de interrogar una cadena de errores, y se usan para cosas distintas.

errors.Is responde «¿hay este error concreto ahí dentro?». Sirve para los centinelas:

_, err := os.Open("no_existe.txt")
fmt.Println(err)
fmt.Println("¿es ErrNotExist?", errors.Is(err, os.ErrNotExist))
salidaopen no_existe.txt: no such file or directory
¿es ErrNotExist? true

errors.As responde «¿hay un error de este tipo, y me lo das para mirar sus datos?». Sirve cuando el error lleva información:

type ErrPrecioInvalido struct {
	Valor float64
}

func (e *ErrPrecioInvalido) Error() string {
	return fmt.Sprintf("precio inválido: %.2f", e.Valor)
}
err = procesarPedido(2, -5)
fmt.Println(err)

var ep *ErrPrecioInvalido
if errors.As(err, &ep) {
	fmt.Println("el precio que falló era", ep.Valor)
}
salidaprocesando el pedido: precio inválido: -5.00
el precio que falló era -5

Fíjate en que a errors.As se le pasa la dirección de una variable del tipo que buscas: ahí es donde deja el error encontrado.

defer: lo que hay que hacer sí o sí

defer aplaza una llamada hasta que la función termine, salga por donde salga. Es lo que en otros lenguajes resuelve un finally.

func escribir(ruta string, texto string) error {
	f, err := os.Create(ruta)
	if err != nil {
		return fmt.Errorf("creando %s: %w", ruta, err)
	}
	defer f.Close()      // pase lo que pase a partir de aquí, se cierra

	if _, err := f.WriteString(texto); err != nil {
		return fmt.Errorf("escribiendo en %s: %w", ruta, err)
	}
	return nil
}

La costumbre es ponerlo justo después de comprobar que el recurso se abrió. Así el cierre está al lado de la apertura y no hay que acordarse de él en cada return.

Si hay varios, salen en orden inverso, como una pila:

for i := 1; i <= 3; i++ {
	defer fmt.Println("defer", i)
}
fmt.Println("final de la función")
salidafinal de la función
defer 3
defer 2
defer 1

panic: lo que casi nunca vas a usar

Un panic aborta el programa. Lo lanza el propio lenguaje cuando pasa algo imposible —un índice fuera de rango, un puntero nulo—, y se puede interceptar con recover dentro de un defer:

defer func() {
	if r := recover(); r != nil {
		fmt.Println("recuperado de:", r)
	}
}()

var s []int
_ = s[5]
salidarecuperado de: runtime error: index out of range [5] with length 0
el programa sigue vivo
Pero no lo uses como excepción. panic es para fallos de los que no se puede seguir: una configuración imposible al arrancar, un estado que no debería existir. Un archivo que no está o una conexión que falla no son eso, son cosas previstas, y para eso están los errores. Si estás usando recover para controlar el flujo normal, has traído las excepciones a un lenguaje que decidió no tenerlas.

Lo que hay que llevarse

Para Usa
Crear un error fijo y comparable errors.New en una variable ErrAlgo
Crear uno con datos del momento fmt.Errorf("...%v...", x)
Añadir contexto conservando el de dentro fmt.Errorf("...: %w", err)
Preguntar si es un error concreto errors.Is(err, ErrAlgo)
Sacar un error con datos errors.As(err, &miTipo)
Garantizar una limpieza defer, justo tras abrir el recurso
Abortar ante algo imposible panic, y con mucha moderación

Todo el código de esta lección se compiló y ejecutó con Go 1.25 antes de publicarla. El error de os.Open es el real del sistema, y el panic se provocó de verdad accediendo a un índice fuera de rango.