Manejo de errores en Zig
En el desarrollo de software, el manejo de errores es fundamental para crear aplicaciones robustas. Zig aborda este aspecto de una manera única y pragmática: no hay excepciones, los errores son valores. Este enfoque permite un control preciso sobre cómo y cuándo se manejan los errores, haciendo que el código sea más predecible y fácil de razonar.
Conjuntos de errores (error sets)
Un conjunto de errores en Zig es similar a una enumeración, donde cada error en el conjunto es un valor. Veamos cómo crear un conjunto de errores:
const ErroresDeArchivo = error{
AccesoDenegado,
MemoriaInsuficiente,
ArchivoNoEncontrado,
};
Este código define un tipo ErroresDeArchivo
que puede tomar uno de tres valores de error posibles.
Coerción entre conjuntos de errores
Los conjuntos de errores pueden convertirse implícitamente a sus superconjuntos. Un superconjunto es un conjunto que incluye todos los errores del conjunto original y posiblemente más:
const expect = @import("std").testing.expect;
const ErrorDeAsignacion = error{MemoriaInsuficiente};
const ErroresDeArchivo = error{
AccesoDenegado,
MemoriaInsuficiente,
ArchivoNoEncontrado,
};
test "coerción de un subconjunto a un superconjunto" {
const err: ErroresDeArchivo = ErrorDeAsignacion.MemoriaInsuficiente;
try expect(err == ErroresDeArchivo.MemoriaInsuficiente);
}
En este ejemplo, ErrorDeAsignacion
es un subconjunto de ErroresDeArchivo
porque todos sus errores (solo MemoriaInsuficiente
) están también en ErroresDeArchivo
. Esto permite la coerción automática mostrada.
Uniones de error
Una unión de error es un tipo que puede contener o bien un valor normal, o bien un error. Se crea combinando un conjunto de errores y otro tipo mediante el operador !
:
test "unión de error" {
const posible_error: ErrorDeAsignacion!u16 = 10;
const sin_error = posible_error catch 0;
try expect(@TypeOf(sin_error) == u16);
try expect(sin_error == 10);
}
Aquí, posible_error
es una unión de error que puede contener un u16
o un ErrorDeAsignacion
. El operador catch
se utiliza para proporcionar un valor alternativo cuando se produce un error.
Funciones que retornan errores
Las funciones en Zig suelen devolver uniones de error. Veamos un ejemplo:
fn funcionQueFalla() error{Ups}!void {
return error.Ups;
}
test "retornando un error" {
funcionQueFalla() catch |err| {
try expect(err == error.Ups);
return;
};
}
En este caso, la sintaxis |err|
captura el valor del error. Esta técnica, llamada "captura de carga útil" (payload capturing), se utiliza en varias partes de Zig. Es importante destacar que, a diferencia de otros lenguajes, esta sintaxis no se utiliza para lambdas en Zig.
La palabra clave try
Zig proporciona la palabra clave try
como una forma concisa de propagar errores. try x
es equivalente a x catch |err| return err
:
fn funcionConError() error{Ups}!i32 {
try funcionQueFalla();
return 12;
}
test "uso de try" {
const v = funcionConError() catch |err| {
try expect(err == error.Ups);
return;
};
try expect(v == 12); // nunca se alcanza
}
Es importante entender que try
y catch
en Zig no están relacionados con las construcciones try-catch de otros lenguajes. Son mecanismos específicos para el manejo de uniones de error.
La palabra clave errdefer
Zig ofrece errdefer
, que funciona como defer
pero solo se ejecuta cuando la función retorna con un error dentro del bloque errdefer
:
var problemas: u32 = 98;
fn contadorDeFallos() error{Ups}!void {
errdefer problemas += 1;
try funcionQueFalla();
}
test "errdefer" {
contadorDeFallos() catch |err| {
try expect(err == error.Ups);
try expect(problemas == 99);
return;
};
}
Esta característica es particularmente útil para realizar acciones de limpieza solo cuando ocurre un error.
Conjuntos de errores inferidos
Las uniones de error devueltas por una función pueden tener sus conjuntos de errores inferidos si no se especifica un conjunto explícito. Este conjunto inferido contiene todos los posibles errores que la función puede devolver:
fn crearArchivo() !void {
return error.AccesoDenegado;
}
test "conjunto de errores inferido" {
// la coerción de tipo se realiza correctamente
const x: error{AccesoDenegado}!void = crearArchivo();
// Zig no nos permite ignorar uniones de error mediante _ = x;
// debemos desenvolverlo con "try", "catch" o "if" de alguna manera
_ = x catch {};
}
Esta característica permite escribir funciones sin especificar explícitamente todos los posibles errores, mientras que el sistema de tipos de Zig se encarga de garantizar que todos los errores se manejen correctamente.
Fusión de conjuntos de errores
Los conjuntos de errores se pueden combinar mediante el operador ||
:
const A = error{ NoEsDirectorio, RutaNoEncontrada };
const B = error{ MemoriaInsuficiente, RutaNoEncontrada };
const C = A || B;
El resultado es un nuevo conjunto de errores que contiene la unión de todos los errores de ambos conjuntos.
El conjunto de errores global
Zig proporciona anyerror
, que es el conjunto global de errores. Por ser el superconjunto de todos los conjuntos de errores, puede recibir cualquier error por coerción:
fn funcionConCualquierError() anyerror!void {
return error.AlgunErrorPersonalizado;
}
Sin embargo, su uso generalmente debe evitarse, ya que pierde la especificidad que hace que el manejo de errores en Zig sea tan poderoso. Es mejor definir conjuntos de errores específicos para cada contexto.
Conclusión
El enfoque de Zig para el manejo de errores, tratándolos como valores y utilizando uniones de error, proporciona una manera clara y explícita de manejar situaciones excepcionales. A diferencia de las excepciones en otros lenguajes, los errores en Zig son parte del sistema de tipos, lo que permite detectar muchos problemas potenciales en tiempo de compilación.
Las construcciones como try
, catch
y errdefer
facilitan el trabajo con errores, mientras que la inferencia de conjuntos de errores y la coerción entre conjuntos añaden flexibilidad sin sacrificar la seguridad. Todo esto hace que el manejo de errores en Zig sea a la vez potente y pragmático, contribuyendo a la creación de software más robusto.