Los operadores catch y try son las herramientas básicas para consumir y propagar errores en Zig. En el artículo anterior vimos que una función puede devolver una unión de error (!T): puede contener el dato que queremos o un error.
El compilador de Zig es estricto: no te dejará usar el valor sin antes comprobar si es un error. Tienes que “gestionar el paquete”.
Para ello, Zig nos da tres herramientas principales: catch, try y el if con captura. Veamos cómo usarlas para escribir código robusto y lineal.
El operador catch
catch es la forma más directa de manejar un error. Se lee casi como inglés: “Intenta hacer esto, y si falla, captura el error y haz esto otro”.
Se usa principalmente cuando queremos recuperarnos del fallo proporcionando un valor por defecto.
fn contarLineas() !u32 {
return error.ArchivoNoEncontrado;
}
pub fn main() void {
// Si contarLineas falla, usamos el 0
const lineas = contarLineas() catch 0;
std.debug.print("Líneas: {d}\n", .{lineas});
}Para que esto funcione, el tipo de dato de la derecha (el 0) debe coincidir con el tipo que la función prometía devolver (u32).
catch con bloques
A veces, la recuperación es más compleja que simplemente devolver un número fijo. Quizás necesitamos loguear el error antes de decidir qué hacer. En ese caso, catch acepta un bloque de código.
const numero = funcionFalible() catch |err| recuperacion: {
std.debug.print("Uy, falló: {any}. Usando valor por defecto.\n", .{err});
// El bloque debe devolver un valor compatible
break :recuperacion 42;
};La parte clave es |err|. Al igual que en los bucles for, podemos capturar el error real para inspeccionarlo (por ejemplo, en un switch) y tomar decisiones diferentes según el tipo de fallo.
La palabra clave try
En la mayoría de los casos, cuando una función falla, no sabemos cómo arreglarlo “in situ”. Lo único sensato que podemos hacer es devolver el error a quien nos llamó para que él decida.
En lenguajes como Go, esto genera el famoso boilerplate:
// Estilo Go (NO es Zig)
res, err := funcion()
if err != nil {
return err
}En Zig, tenemos la palabra clave try.
fn funcionIntermedia() !void {
// Si funcionPeligrosa() da error, 'try' lo devuelve AUTOMÁTICAMENTE
// Si tiene éxito, 'try' extrae el valor y lo guarda en 'x'
const x = try funcionPeligrosa();
std.debug.print("Éxito: {d}\n", .{x});
}try no es un bloque try-catch como en Java o JavaScript.
Es un operador de prefijo que significa: “Si esto es error, haz return error ahora mismo. Si no, dame el valor limpio”.
Esto mantiene el código plano y legible. Puedes encadenar operaciones que fallan sin anidar varios if.
fn procesarArchivo() !void {
const file = try abrirArchivo(); // Puede fallar y retornar
const datos = try leerDatos(file); // Puede fallar y retornar
try procesar(datos); // Puede fallar y retornar
}if con captura de error
A veces no queremos devolver el error (try) ni usar un valor por defecto (catch). Queremos bifurcar la lógica: “Si va bien haz A, si va mal haz B”.
Para esto usamos el if sobre el valor devuelto, capturando el error en el else.
if (funcionFalible()) |valor| {
std.debug.print("Todo salió bien: {d}\n", .{valor});
} else |err| {
std.debug.print("Falló con el error: {any}\n", .{err});
// Aquí podemos manejar el error sin salir de la función
if (err == error.ConexionPerdida) {
reconectar();
}
}Esta estructura es idéntica a la que vimos con los Opcionales (if (opt) |v| else ...). Es la forma consistente de Zig de manejar “resultados inciertos”.
catch unreachable
Hay situaciones donde, teóricamente, una función devuelve un error (porque su firma lo dice), pero nosotros sabemos con certeza absoluta que en este contexto no va a fallar.
Si ignoramos el error, el código no compila.
Si ponemos un try, tenemos que cambiar la firma de nuestra función main o actual.
La solución es decirle al compilador: “Si esto falla, es un bug imposible. Crashea el programa.”
const resultado = funcionQueNoDeberiaFallar() catch unreachable;Si la función falla, se alcanza unreachable: en Debug y ReleaseSafe produce un panic, mientras que ReleaseFast y ReleaseSmall no conservan necesariamente esa comprobación. Úsalo solo cuando el error revele una precondición rota o un fallo del programa.
Comparación de herramientas
| Herramienta | Sintaxis | Significado | Uso típico |
|---|---|---|---|
| Catch Default | func() catch 0 | “Si falla, usa 0” | Valores por defecto, recuperación simple. |
| Try | try func() | “Si falla, retorna el error” | Propagación de errores hacia arriba. |
| If Error | `if (func()) | v | else |
| Unreachable | func() catch unreachable | “Fallar aquí rompe una precondición” | Invariantes comprobadas. |
Uniendo piezas: defer, errdefer y errores
Ahora que conocemos defer y el manejo de errores, podemos combinarlos mediante errdefer.
errdefer funciona igual que defer, pero solo se ejecuta si la función devuelve un error.
Sirve para limpiar recursos adquiridos durante una construcción que no llega a completarse.
fn crearUsuarioComplejo() !*Usuario {
const user = crearEstructura();
// Si algo falla a partir de aquí, destruye la estructura user
errdefer destruirEstructura(user);
try pasoPeligroso1(user);
try pasoPeligroso2(user);
// Si llegamos aquí, todo ha ido bien.
// El errdefer NO se ejecuta, así que devolvemos el usuario vivo.
return user;
}