← Volver al listado de tecnologías

Manejo de Errores en Zig

Por: Artiko
zigerrorestrycatcherror-handling

Manejo de Errores

Error Sets

const std = @import("std");

// Definir conjunto de errores
const FileError = error{
    NotFound,
    PermissionDenied,
    IoError,
};

const ParseError = error{
    InvalidFormat,
    OutOfRange,
    UnexpectedToken,
};

// Combinar error sets
const AppError = FileError || ParseError;

fn ejemplo() AppError!void {
    return AppError.NotFound;
}

Error Unions

const std = @import("std");

const MathError = error{
    DivisionByZero,
    Overflow,
    Underflow,
};

fn dividir(a: i32, b: i32) MathError!i32 {
    if (b == 0) return MathError.DivisionByZero;
    return @divTrunc(a, b);
}

fn raizCuadrada(n: f64) error{NegativeNumber}!f64 {
    if (n < 0) return error.NegativeNumber;
    return @sqrt(n);
}

pub fn main() void {
    // Usando catch para manejar error
    const resultado = dividir(10, 2) catch |err| {
        std.debug.print("Error: {}\n", .{err});
        return;
    };
    std.debug.print("Resultado: {d}\n", .{resultado});

    // catch con valor por defecto
    const r2 = dividir(10, 0) catch 0;
    std.debug.print("Con default: {d}\n", .{r2});
}

Try y Propagación

const std = @import("std");

const Error = error{
    InvalidInput,
    ProcessingError,
    OutputError,
};

fn paso1(input: i32) Error!i32 {
    if (input < 0) return Error.InvalidInput;
    return input * 2;
}

fn paso2(input: i32) Error!i32 {
    if (input > 1000) return Error.ProcessingError;
    return input + 100;
}

fn paso3(input: i32) Error!i32 {
    if (input == 0) return Error.OutputError;
    return input;
}

// try propaga el error automáticamente
fn pipeline(input: i32) Error!i32 {
    const r1 = try paso1(input);
    const r2 = try paso2(r1);
    const r3 = try paso3(r2);
    return r3;
}

pub fn main() void {
    if (pipeline(50)) |resultado| {
        std.debug.print("Éxito: {d}\n", .{resultado});
    } else |err| {
        std.debug.print("Error en pipeline: {}\n", .{err});
    }
}

Errdefer

const std = @import("std");

const Recurso = struct {
    datos: []u8,
    allocator: std.mem.Allocator,

    pub fn init(allocator: std.mem.Allocator, size: usize) !Recurso {
        const datos = try allocator.alloc(u8, size);
        return Recurso{ .datos = datos, .allocator = allocator };
    }

    pub fn deinit(self: *Recurso) void {
        self.allocator.free(self.datos);
    }
};

fn crearRecursos(allocator: std.mem.Allocator) !struct { r1: Recurso, r2: Recurso } {
    var r1 = try Recurso.init(allocator, 100);
    errdefer r1.deinit(); // Se ejecuta solo si hay error después

    var r2 = try Recurso.init(allocator, 200);
    errdefer r2.deinit();

    // Si algo falla aquí, r1 y r2 se limpian automáticamente
    if (r1.datos.len + r2.datos.len > 250) {
        return error.TooMuchMemory;
    }

    return .{ .r1 = r1, .r2 = r2 };
}

pub fn main() !void {
    var gpa = std.heap.GeneralPurposeAllocator(.{}){};
    defer _ = gpa.deinit();

    var recursos = try crearRecursos(gpa.allocator());
    defer recursos.r1.deinit();
    defer recursos.r2.deinit();

    std.debug.print("Recursos creados exitosamente\n", .{});
}

Patrones Comunes

const std = @import("std");

const ValidationError = error{
    EmptyString,
    TooLong,
    InvalidCharacter,
};

fn validarNombre(nombre: []const u8) ValidationError!void {
    if (nombre.len == 0) return ValidationError.EmptyString;
    if (nombre.len > 50) return ValidationError.TooLong;

    for (nombre) |c| {
        if (!std.ascii.isAlphabetic(c) and c != ' ') {
            return ValidationError.InvalidCharacter;
        }
    }
}

// Patrón: convertir error a optional
fn buscarSeguro(arr: []const i32, valor: i32) ?usize {
    for (arr, 0..) |elem, i| {
        if (elem == valor) return i;
    }
    return null;
}

// Patrón: unwrap con mensaje
fn unwrapConPanic(valor: anyerror!i32) i32 {
    return valor catch |err| {
        std.debug.panic("Error inesperado: {}", .{err});
    };
}

// Patrón: retry
fn operacionConReintentos(intentos: u32) !i32 {
    var i: u32 = 0;
    while (i < intentos) : (i += 1) {
        if (realizarOperacion()) |resultado| {
            return resultado;
        } else |_| {
            std.time.sleep(100 * std.time.ns_per_ms);
        }
    }
    return error.MaxRetriesExceeded;
}

fn realizarOperacion() !i32 {
    // Simulación
    return error.TemporaryFailure;
}

pub fn main() void {
    // Validación
    validarNombre("Juan") catch |err| {
        std.debug.print("Nombre inválido: {}\n", .{err});
    };

    // Optional
    const arr = [_]i32{ 1, 2, 3 };
    if (buscarSeguro(&arr, 2)) |idx| {
        std.debug.print("Encontrado en: {d}\n", .{idx});
    }
}

Error Trace

const std = @import("std");

fn nivel3() !void {
    return error.ErrorEnNivel3;
}

fn nivel2() !void {
    try nivel3();
}

fn nivel1() !void {
    try nivel2();
}

pub fn main() void {
    nivel1() catch |err| {
        std.debug.print("Error capturado: {}\n", .{err});

        // En modo debug, puedes obtener el stack trace
        if (@errorReturnTrace()) |trace| {
            std.debug.dumpStackTrace(trace.*);
        }
    };
}

Testing de Errores

const std = @import("std");
const testing = std.testing;

const ParseError = error{
    InvalidNumber,
    OutOfRange,
    EmptyInput,
};

fn parsearNumero(input: []const u8) ParseError!i32 {
    if (input.len == 0) return ParseError.EmptyInput;

    var resultado: i32 = 0;
    for (input) |c| {
        if (c < '0' or c > '9') return ParseError.InvalidNumber;
        resultado = resultado * 10 + @as(i32, c - '0');
        if (resultado < 0) return ParseError.OutOfRange; // overflow
    }
    return resultado;
}

fn dividirSeguro(a: i32, b: i32) error{DivisionByZero}!i32 {
    if (b == 0) return error.DivisionByZero;
    return @divTrunc(a, b);
}

fn validarRango(n: i32, min: i32, max: i32) error{OutOfRange}!i32 {
    if (n < min or n > max) return error.OutOfRange;
    return n;
}

test "parsear número válido" {
    const resultado = try parsearNumero("123");
    try testing.expectEqual(@as(i32, 123), resultado);
}

test "parsear número cero" {
    const resultado = try parsearNumero("0");
    try testing.expectEqual(@as(i32, 0), resultado);
}

test "parsear input vacío retorna error" {
    const resultado = parsearNumero("");
    try testing.expectError(ParseError.EmptyInput, resultado);
}

test "parsear caracteres inválidos retorna error" {
    try testing.expectError(ParseError.InvalidNumber, parsearNumero("12a3"));
    try testing.expectError(ParseError.InvalidNumber, parsearNumero("abc"));
}

test "división exitosa" {
    const resultado = try dividirSeguro(10, 2);
    try testing.expectEqual(@as(i32, 5), resultado);
}

test "división por cero" {
    try testing.expectError(error.DivisionByZero, dividirSeguro(10, 0));
}

test "validar rango exitoso" {
    const resultado = try validarRango(50, 0, 100);
    try testing.expectEqual(@as(i32, 50), resultado);
}

test "validar rango fuera de límites" {
    try testing.expectError(error.OutOfRange, validarRango(-1, 0, 100));
    try testing.expectError(error.OutOfRange, validarRango(101, 0, 100));
}

test "encadenar operaciones con try" {
    const paso1 = try dividirSeguro(100, 2);
    const paso2 = try validarRango(paso1, 0, 100);
    try testing.expectEqual(@as(i32, 50), paso2);
}

test "catch con valor por defecto" {
    const resultado = dividirSeguro(10, 0) catch -1;
    try testing.expectEqual(@as(i32, -1), resultado);
}

test "if con error union" {
    if (parsearNumero("42")) |valor| {
        try testing.expectEqual(@as(i32, 42), valor);
    } else |_| {
        try testing.expect(false); // No debería llegar aquí
    }
}

Ejercicios

  1. Implementa un parser de JSON simple que retorne errores descriptivos
  2. Crea una función que lea un archivo y maneje todos los posibles errores
  3. Implementa un validador de email con errores específicos para cada caso

Resumen