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.

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.

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.

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
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.

1 Like

In engineering practice, redefer only supports integer for() loops and is restricted to capturing the index variable i. Within an if statement, redefer can only capture the boolean value of the current block’s condition. These limitations make backend optimization straightforward. In reality, what is pushed onto the stack is not the local variable itself. Instead, under these constraints, the backend effectively re-executes the for loop’s traversal and the if check. As a result, the required stack space is known at compile time.

This principle also holds true for nested loops under these restrictions. In fact, no new backend compilation code is required. redefer can simply be treated as syntactic sugar for defer, by directly translating redefer statements into defer statements at compile time.

redefer as you describe it would allow for unbounded stack growth (your LIFO is about as safe as a VLA), which would conflict with the following proposals:

Some other related proposals to provide context as to why unbounded stack growth isn’t likely to be something the language ever implements.

Pls try to keep discussion civil, we are all engaging in good faith, if we weren’t then we’d be wasting our own time.


Some information you are certainly missing is that local variable lifetimes are supposed to be scope based, not function based. Currently, the compiler does function based lifetimes, but that is just a bug and will be fixed eventually.

Your proposal requires function based lifetimes, which requires that you change the opinion of Andrew, good luck :stuck_out_tongue: it’s not easy.


your example suggests that it works within loops

how does this work if the loop bounds are runtime known? Defers are inserted statically before each return/break/end of scope. If you want it to work with runtime loops then that would be a BIG change in how the compiler works.

How does this work with local variables in loops? Currently, they are overwritten each iteration.

I see you answered those by limiting to comptime known bounds and not referencing in-loop variables. With those restrictions it should be possible and potentialy easier to generate a defer for


Lastly, how does it work when returning inside the loop/nested loop? It would need to dynamically adjust to how many iterations there have been, which may be difficult to implement.


regarding your arguments, not the idea itself

In the context of the previous paragraph I can see how it looks that way. But it is inaccurate, errdefer is still scope based, it just limits where it is triggered to returning an error. errdefer is a special case of defer.
Your redefer is more of an alternative to defer, but can be implemented using defer in the compiler making it a special case of defer.

This is somewhat subjective, @marler8997 demonstrated that it is trivial for your example.

While redefer may help with this, I would argue that it is an issue with the programmers design.

Reusing your 2d array example, you can remove a layer of allocation with some simple maths:

// you would want to make a type to handle this safely
const width = 2;
const height = 2;
var a = try allocator.alloc(f64, width * height);
// might not be neccessary, if no errors could be returned after this
// you can assert that with `errdefer comptime unreachable`
errdefer allocator.free(a);

// pretend this is a fn get(x,y)
const x = 0;
const y = 2;
// row major: [ row0[0, 1], row1[0, 1] ]
a.items[y * width + x];
// collumn major: [ collumn0[0, 1], collumn1[0, 1] ]
a.items[x * height + y];

I agree with this! But in my subjective experience cases complex enough to warrant a something like redefer are not common.

in response to: Proposal: A New redefer Keyword for Function-Scoped Deferred Execution - #3 by marler8997

You can’t just make statements like this without explaining them, it may be obvious to you, but it isn’t to everyone else.

What implicit conditions does it rely on? What changes make it ineffective?

Keeping code concise and not liking length code are very subjective preferences, why should zig cater to your preferences?

A better argument: currently, there can be a detachment from the defer and the code It’s related to, this hampers readability and may result in forgetting to update either of them. This is the same logic that justifies defer and errdefer.


some relevant thoughts that didn’t fit with any of the above sections

I don’t think redefer is clear enough to be the keyword.

It isn’t clear if it runs in the parent scope or in the root scope of the function?

If the latter than fndefer would be a good keyword.

If the former than perhaps defer :label with a labelled scope?
Should this allow going multiple scopes out, or be restricted to only 1 or some other limit.
Would the label be the scope it runs in, or the scope it escapes?
The former would require special case to run in the root fn scope, so the latter may be preferable.

Should there be an err variant? It’s possible that makes more sense than a non err variant?

6 Likes

Rant incoming! Seriously this is messed up.

That’s a serious footgun to leave open! I’ve seen loops allocating and added to a list with defer free and the value being used outside the loop. If this works due to a bug it’s a massive footgun! I’m pretty sure I’ve seen this pattern and copied it into my code somewhere. I probably read that defer is scope based, but let’s not pretend the sparse docs to be gospel in one area and completely missing in another. When the debug allocator with it’s zeroing of freed memory confirms the loop is good it teaches my brain that this is a valid pattern.

The fix would be to loop the list in the function scoped defer and free the elements before the list but suddenly I have to do bookkeeping since not all of my allocations are of the same size. On top the flow becomes wieldy and hard to read.

Having a key feature like defer being implemented incorrectly for years, that’s troubling. Just saying!

Edit: As others have pointed out my use case would be better suited for a temporary arena, that reduces the cleanup substantially by only dealing with blocks instead of allocations. Rant still stands on having a crucial keyword such as defer having such a critical glaring bug though.

I think such a keyword is not justified. The fundamental problem is that this is not a particularly good way to handle local allocations. Managing memory like this, with many small allocations, is going to cause performance problems and, as you discovered, also safety problems.

There are several better ways to handle memory, that may also end up leading to more readable code. The most common one is using an arena:

// It requires some setup, but this can also be moved up in scope, e.g. a game may just have one arena for the entire frame
var arenaAllocator = std.heap.ArenaAllocator.init(allocator);
defer arenaAllocator.deinit();
const arena = arenaAllocator.allocator();

// But the rest of the code becomes almost trivial, no more bookkeeeping
const a = try arena.alloc([]f64, 2);
for(0..2) |i| {
    a[i] = try arena.alloc(f64, 2);
}

Other options may be programming without pointers (using an ArrayList(f64) and storing indices into it instead of slices), preallocating the memory, or using an allocator that cannot fail.

I’d question the need to modify the data in the first place. It seems like a rather niche action, and I don’t see an advantage beyond just using regular defer if the functions is simple or storing an ArrayList of modifications if it’s more complex.

At the end of the day if you really need it, you can also just implement the redefer functionality in user space using an ArrayList and regular defer.

2 Likes