Proposal: A New redefer Keyword for Function-Scoped Deferred Execution

I would like to propose the introduction of a new keyword in Zig: redefer. Its purpose would be to push a block of code onto a LIFO stack for execution when the function scope is exited, whether through a return, a try, or by reaching the end of the function body.

Many of the current challenges in Zig concerning memory management and logical correctness stem from the lack of a dynamic, deferred execution mechanism that pairs seamlessly with return-based control flow. While the existing defer keyword is excellent for block-scoped cleanup (reacting to continue or break), it falls short when dealing with function-level resource management, especially in dynamic scenarios like loops. This inadequacy often leads to subtle and dangerous bugs latent in Zig code.

In fact, errdefer can be seen as a special case of the proposed redefer. The underlying behavior of the try keyword is effectively a return from the current function. Therefore, redefer could completely subsume the functionality of errdefer, allowing for more complex logic by inspecting the return value if needed. Of course, given the convenience of the try keyword, it would still be valuable to keep errdefer as a more concise syntactic sugar for the error path(Because the try statement does not explicitly catch errors).

Consider the following well-known example in the Zig community, which has a serious memory leak issue:

var a = try allocator.alloc([]f64, 2);
errdefer allocator.free(a);
for (0..2) |i| {
    a[i] = try allocator.alloc(f64, 2);
}
defer {
    for (a) |row| allocator.free(row);
}

If the second alloc call fails, a memory leak is guaranteed. Implementing the correct cleanup logic for all failure paths using only defer and errdefer becomes exceptionally complex and unwieldy. This not only reduces development productivity but also explains why this pattern of potentially unsafe code is prevalent across many Zig projects.

With the proposed redefer keyword, the solution becomes elegantly simple:

var a = try allocator.alloc([]f64, 2);
defer allocator.free(a);
for (0..2) |i| {
    a[i] = try allocator.alloc(f64, 2);
    redefer allocator.free(a[i]);
}

Furthermore, the addition of redefer would enable a powerful “side-effect-free” programming style. It’s common for a function to accept a container or a pointer and temporarily modify the data it points to for performance or algorithmic reasons. However, developers often forget to restore the data to its original state before returning, leading to subtle bugs when the modified data is used elsewhere. These types of logical errors can be notoriously difficult to debug.

With redefer, we can schedule the data to be restored immediately after it is modified, ensuring the function is free of side effects from the caller’s perspective. This can be achieved with minimal code overhead.

fn processData(data: []u8) void {
    const original_byte = data[0];
    {//Simulate a scope
      data[0] = 0; // Temporary modification for processing
      redefer data[0] = original_byte; // Guaranteed restoration on function exit
    }
    // ... complex logic that might return early ...
}

I believe this feature would significantly enhance the safety and stability of Zig code while preserving its performance, by providing a robust and intuitive tool for managing resources and state across the entire function’s lifecycle.

To achieve the functionality of this keyword, it seems that it also involves capturing local scope variables.

With the proposed redefer keyword, the solution becomes elegantly simple:

It’s not clear whether your example is correct. If your code is inside a block, then the container array gets freed before the inner arrays (since defer would run at the end of the block but redefer runs at the end of the function.

Having a different kind of defer seems like it could be a footgun. You can also simulate what your proposed redefer does today with something like this:

// put this at the top of the function
var foo: ?T = null;
defer if (foo) {
    // do cleanup
}

// ...later on in the function

foo = ...; // equivalent to rdefer

Implementing the correct cleanup logic for all failure paths using only defer and errdefer becomes exceptionally complex and unwieldy.

Here’s my attempt:

    var al: std.ArrayListUnmanaged([]f64) = .{};
    defer al.deinit(allocator);
    errdefer for (al.items) |row| {
        allocator.free(row);
    };
    try al.ensureTotalCapacity(allocator, 2);
    for (0..2) |_| {
        al.appendAssumeCapacity(try allocator.alloc(f64, 2));
    }

This should behave correctly and even does so if it’s inside a block.

P.S. actually your example may not be correct even if it’s not inside a sub-block. It would depend on if defer’s and rdefer’s get executed in the same reverse order, or if you first execute all defers and then all rdefers…if it’s the latter then example would still be incorrect.

1 Like

I don’t quite understand your first paragraph. In your second paragraph, the solution you provided actually relies on many implicit conditions. In fact, as long as I make slight changes to the requirements and logic, your method will become ineffective. Because fundamentally, to keep code concise, it is necessary to have a corresponding defer statement with the return statement. Secondly, the method you provided is not obviously more lengthy than the redefer method, which is a problem that developers can easily cause.
As for the sequence issue you mentioned, of course it’s a first in, last out, which shouldn’t be a problem, right?


I seem to understand what you mean, you seem to have ambiguity in very details (which I think are irrelevant because they are simple), and then question the entire direction. I don’t want to argue about the details anymore because they are too simple and I thought they didn’t need attention.

The issue you mentioned theoretically exists, but your solution has potential risks. Please see the function below. Since redefer executes whenever a function scope encounters an error, regardless of the timing, here at try bar();, redefer will still be triggered even if the lifetime of a has ended.

fn foo() !void {
    blk: {
        var a = try allocator.alloc([]f64, 2);
        errdefer allocator.free(a);
        for (0..2) |i| {
            a[i] = try allocator.alloc(f64, 2);
            redefer allocator.free(a[i]);
        }
        defer {
            for (a) |row| allocator.free(row);
        }
    }
    try bar();
}

Edit:
If your redefer is strictly limited to the ‘errdefer of the entire loop level’ rather than the ‘function-level errdefer,’ it might be feasible. One possible translation is as follows:

    var a = try allocator.alloc([]f64, 2);
    errdefer allocator.free(a);
    var i, const maybe_err = blk: {
        for (0..2) |i| {
            a[i] = allocator.alloc(f64, 2) catch |err| break :blk .{ i, err };
        }
        break :blk .{ undefined, {} };
    };
    maybe_err catch |err| {
        while (i > 0) {
            i -= 0;
            allocator.free(a[i]);
        }
        return err;
    };

    defer {
        for (a) |row| allocator.free(row);
    }

However, in order for it to always translate correctly and feasibly within loops, I think it would at least require support at the language level for things like reverse iterators. In simple range-based for loops, the problem is not as obvious.

As for the sequence issue you mentioned, of course it’s a first in, last out, which shouldn’t be a problem, right?

If that’s the case then you’ve introduced some “weirdness”. Take this example:

defer foo();
redefer bar();

What executes first, foo or bar? We actually don’t have enough information to tell. If this code appears at the top-level of a function, then it will be bar then foo (LIFO order). However, if this appears inside a block like this:

fn func() void {
    {
        defer foo();
        redefer bar();
    }
}

Now in this case the order will be foo then bar. Having the order of deferred execution depend on which level of a function the code exists is the footgun I was referring to. In your code example, if your code appeared anywhere other than the top-level, it would have reversed the cleanup order and you’d have a bug.

Because of this, I was thinking you might have been proposing the alternative, where you always execute all defers and then finish with all the rdefers, but in that case your code example would be broken from the get-go. So, I’m actually not sure how you were wanting this to behave and…I’m not sure how you could even fix your example to work correctly.

what are you saying?Do you want to blame the logical problem on keywords? Do you want to use a mistake to deny the person who provided the tool?

1 Like

I am too tired to answer some meaningless questions. Looking forward to better safety and stability for Zig. Wishing you all the best.