zig-errdefer-limpieza-condicional

errdefer en Zig: limpieza condicional al devolver un error

  • 4 min

La sentencia errdefer es una variante de defer que limpia al propagar un error desde su ámbito. En el artículo anterior vimos que defer actúa ante cualquier salida normal del bloque; ahora veremos cómo reservar una limpieza para las rutas de error.

¿Qué ocurre cuando estamos construyendo un objeto complejo y queremos transferirlo al llamante solo si queda completo?

Supongamos que una función crearCoche() tiene que hacer dos cosas:

  1. Comprar el motor (reserva memoria).
  2. Pintar la carrocería (operación que puede fallar).

Si usamos defer destruir(motor), el motor se destruirá siempre al final de la función, ¡incluso si todo salió bien! Y si no ponemos nada, y el paso 2 falla, nos quedaremos con un motor “colgando” (memory leak) porque nunca llegamos al return.

Para eso Zig nos da errdefer.

El concepto de rollback

errdefer le dice al compilador: “Ejecuta esta línea de limpieza SOLO si la función termina devolviendo un error”.

Si la función tiene éxito y devuelve un valor normal (haciendo un return limpio), el errdefer se ignora.

Esto nos permite aplicar una semántica transaccional a la gestión de memoria: o creamos el objeto completo o deshacemos las reservas parciales.

Ejemplo práctico: construcción en varios pasos

Vamos a ver el caso clásico de asignación de memoria múltiple. Queremos crear un Jugador que tiene un nombre (string dinámico) y un inventario (array dinámico).

const Jugador = struct {
    nombre: []u8,
    inventario: []u8,
};

fn crearJugador(allocator: Allocator) !*Jugador {
    // 1. Reservamos memoria para el struct base
    const jugador = try allocator.create(Jugador);
    // Si fallamos más adelante, liberamos el struct jugador
    errdefer allocator.destroy(jugador);

    // 2. Reservamos memoria para el nombre
    // Si esto falla, salta el errdefer anterior y libera 'jugador'
    jugador.nombre = try allocator.alloc(u8, 10);
    
    // Ahora tenemos DOS cosas que limpiar si el siguiente paso falla.
    // Añadimos otro errdefer para el nombre.
    errdefer allocator.free(jugador.nombre);

    // 3. Reservamos memoria para el inventario (Paso peligroso final)
    // Si esto falla, se ejecutan los dos errdefer en orden inverso (LIFO)
    jugador.inventario = try allocator.alloc(u8, 100);

    // 4. ¡ÉXITO!
    // Llegamos al final. Devolvemos el puntero.
    // Los 'errdefer' se DESACTIVAN y no se ejecutan.
    return jugador;
}
Copied!

Analicemos el flujo:

  • Si falla el paso 1 (create): La función retorna error inmediatamente. No hay errdefer activos aún.
  • Si falla el paso 2 (alloc nombre): Se ejecuta destroy(jugador) y se retorna error.
  • Si falla el paso 3 (alloc inventario): Se ejecuta free(nombre), luego destroy(jugador) y se retorna error.
  • Si todo va bien: No se ejecuta ninguna limpieza. El llamante recibe un objeto completo y ahora es su responsabilidad limpiarlo (probablemente con una función destroyJugador).

Diferencia clave con defer

Es importante no confundirlos: elegir el incorrecto puede liberar un resultado válido o dejar una reserva sin liberar.

SentenciaCondición de ejecuciónUso típico
deferAl salir del bloque por el flujo normal, incluido return o try.Cerrar archivos, desbloquear mutex, liberar memoria temporal local.
errdeferAl propagar un error mientras sigue activo en su ámbito.Deshacer cambios parciales en objetos que pretendemos devolver.

Combinándolos

Es muy común ver funciones que usan ambos. defer para variables temporales que solo sirven dentro de la función, y errdefer para la construcción del resultado.

fn leerConfiguracion(allocator: Allocator) !*Config {
    // Abrimos un archivo temporal (necesitamos cerrarlo siempre al acabar la función)
    var file = try openFile("temp.txt");
    defer file.close(); 

    // Reservamos memoria para la config (queremos devolverla, no borrarla si va bien)
    const config = try allocator.create(Config);
    errdefer allocator.destroy(config);

    // Leemos del archivo y llenamos la config
    try rellenarConfiguracion(file, config);

    return config;
}
Copied!

errdefer en bloques

Al igual que defer, errdefer está ligado al ámbito donde se declara.

Solo queda activo después de que la ejecución pase por su declaración. Si el ámbito termina normalmente, se desactiva; si desde él se propaga un error, ejecuta la limpieza antes de abandonarlo.

Un errdefer declarado dentro del cuerpo de un for pertenece a esa iteración. Si la iteración termina normalmente se desactiva; si propaga un error, limpia los recursos adquiridos durante esa vuelta.

El patrón init y deinit

El uso de errdefer encaja con un patrón habitual en Zig: init y deinit.

En lugar de tener constructores que lanzan excepciones a medias, en Zig solemos escribir:

  1. init(): Crea el objeto. Usa errdefer internamente para asegurar que si falla, no devuelve nada sucio.
  2. deinit(): Limpia el objeto completo.

Esto garantiza que, desde fuera, el objeto tiene dos estados posibles: totalmente válido o totalmente inexistente. Nunca un estado “zombi” a medio inicializar.