Skip to content
New issue

Have a question about this project? # for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “#”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? # to your account

errdefer should be a compile error when used inside a function that doesn't return an error #2654

Open
ghost opened this issue Jun 11, 2019 · 8 comments
Labels
accepted This proposal is planned. proposal This issue suggests modifications. If it also has the "accepted" label then it is planned.
Milestone

Comments

@ghost
Copy link

ghost commented Jun 11, 2019

This code is currently allowed, but from reading the docs I don't think it's ever possible for that errdefer to be hit.

pub fn main() void {
    var x = false;

    errdefer x = true;
}
@daurnimator
Copy link
Contributor

I once had the same thought, but it came in useful when coding defensively:

fn myfunc() Foo {
    const foo = Foo.init();
    errdefer foo.deinit()
    foo.bar = Bar.init();
    return foo;
}

And then at some point later, Bar.init is changed to sometimes throw an error: a developer only needs to add ! to the return signature: the errdefer is already there.
-> I see this as helping the "good practice" that every .init() is paired with a .deinit()

@ghost
Copy link
Author

ghost commented Jun 11, 2019

On the other hand, I ran into this when I was refactoring and ended up with this stale code (the errdefers) that I needed to change to regular defers.

(My refactor was something like taking a large init function, which returned errors, and which was called by my main function - and inlining it into my main function which doesn't return errors. The errdefers were previously in the init function)

@andrewrk andrewrk added this to the 0.6.0 milestone Jun 11, 2019
@andrewrk andrewrk added the proposal This issue suggests modifications. If it also has the "accepted" label then it is planned. label Jun 11, 2019
@CurtisFenner
Copy link

What about functions that do return errors, but don't in the paths following the errdefer, like

fn dothing() !void {
    if (badthing()) {
        return error.AProblemHappened;
    }
    errdefer cleanup();
}

I think if you wanted to complain about errdefer in functions that don't return errors at all you'd also want to complain about the above example.

Though, I do think the point @daurnimator is important. Because Zig doesn't have any "automatic" resource cleanup (such as RAII), it seems like it would be good style to always pair resource acquisition with an errdefer wherever appropriate; however, if no-op errdefer is not allowed, then you would be forced to sometimes omit them:

var r1 = try R1.acquire();
errdefer r1.cleanup();

var r2 = try R2.acquire();
errdefer r2.cleanup();

var r3 = try R3.acquire();
// conspicuously missing errdefer

return consume(r1, r2, r3);

If someone were to modify this code to acquire a fourth resource, they probably wouldn't know whether or not r3 is intentionally missing cleanup (without a comment to the effect of "errdefer is missing because the compiler is annoying"), and in large/complex enough functions, the modifier might not even notice.

@nmichaels
Copy link
Contributor

nmichaels commented Dec 23, 2019

I see the arguments of the people saying that it can be a clean style to have dead code, but it's still dead code. You may as well have

fn dothing() !void {
    if (badthing()) {
        return error.AProblemHappened;
    }
    errdefer unreachable;
}

This relates to the Zig compiler's overall lazy approach to evaluation, where if it doesn't know that an expression is needed in this build it won't try to compile it. Untested code paths are buggy, and if you've got an errdefer block that can't ever be reached, it's untestable. Personally, I'd rather write the code with unreachable and a comment explaining that cleanup() should be called if this changes.

Anyway, I'll try to sum up the debate with examples of bugs from both sides. Either untested code is silently enabled or cleanup isn't done properly.

In the case where a function with no error in its type errdefers:

fn myFunc() Foo {
    const foo = Foo.init();
    errdefer foo.deinit()
    foo.bar = Bar.init();
    errdefer foo.deinit() // <-- See the bug? Here it is.
    const foo.baz = Baz.init();
    return foo;
}

This is not an unlikely scenario (by my estimation) and when myfunc is changed to return !Foo because the programmer changes Bar.init()'s return type to !Bar, there's one chance to catch it when Bar.init() becomes try Bar.init().

In the proposed case:

fn myFunc() Foo {
    const foo = Foo.init();
    foo.bar = Bar.init();
    return foo;
}

When Bar is changed to return !Bar, myfunc has to be changed to return !Foo and Bar.init() gets its try added. The programmer fails to inspect the rest of myfunc and doesn't add the errdefer Foo.init() and now there's a resource leak.

There's another situation, which is how I ended up here: incorrect use of errdefer.

fn myFunc() ?Foo {
    const foo = Foo.init();
    errdefer foo.deinit();
    // We're going to pretend it makes sense for Bar.init() to return null.
    if (Bar.init()) |bar| {
        foo.bar = bar;
    } else {
        return null;
    }
    return foo;
}

Whoops, leaked some memory because Bar.init() returned null and null is not an error. This is just a bug, and it was found with valgrind. I was surprised, though.

All of the above scenarios are bad, and none are unlikely. If I'm already running valgrind on my tests, it will catch all of these situations, but that's only true if the leaked resource is memory. On principle, I prefer the proposal, since I don't like dead code in my codebase.

I totally forgot @dbandstra's case of refactoring that needs to change a return type from !Foo to Foo. It puts some weight on the pro-proposal side.

@hryx
Copy link
Contributor

hryx commented Dec 30, 2019

See also: #335, #282, #952

@andrewrk andrewrk added the accepted This proposal is planned. label Jan 7, 2020
@andrewrk
Copy link
Member

andrewrk commented Jan 7, 2020

Thank you @nmichaels for your well-considered and detailed response here. I agree with your reasoning, as well as your suggestion for what idiomatic zig code would look like when "coding defensively":

    errdefer unreachable;

This will be allowed, even when there is no possible error returned from the function, for the same reason that unreachable is allowed in unreachable code paths.

The idea here is when the programmer gets the compile error for returning a compile error through errdefer unreachable (possibly related: #4094), the programmer should understand that resource handling for errors was not taken into account previously, and should inspect the entire function, adding resource management where appropriate.

Code reviewers can look for the simple pattern of:

-    errdefer unreachable;

And understand that the entire function's resource management with respect to errors, needs to be double-checked.


I also want to note that much like the other issues @hryx noted, multibuilds (#3028) is required to enable this compile error, because the return type and reachability in general could depend on build options.

@GMWolf
Copy link

GMWolf commented Nov 9, 2024

I fail to see any material upsides to this proposal, which seems to me like it is inspired by a dogmatic view against dead code without considering why dead code is harmful.

I first want to point out that errdefer unreachable; is deadcode already.
and worse, errdefer unreachable; // deinit the object here is commented out dead code, written in English.

I don't see any value in telling a future human to add needed defer statements if anything changes, when we already have a perfectly functional way to tell the compiler to add the defer statement if needed. Why involve error prone humans more than necessary?

I am not convinced by the example cases given by @nmichaels.
The errors made in the dead code examples are just as likely to be made after this proposal, except they would be made when dealing with code that you didn't write, making the error more likely.
In fact, after this proposal, these errdefer statements will likely need to be introduced many at a time, further increasing the odds of making such an error when copy/pasting.

So not only does this proposal increase the odds of forgetting a defer, it does not reduce the chances of making that error, all while keeping the dead code around, just commented out and written in English instead of code.

I also want to point out reliance on tools like valgrind to catch missing errdefers only works in a limited number of cases. Not all errdefers manage resources monitored by valgrid, and in real time apps, such as games, valgrind is not a option (even debug builds are often not used either, even during development). nor will valgrind catch coding errors that did not happen during prod or QA. Live issues are very common and retail crash dumps are opaque at best.

@mnemnion
Copy link

I first want to point out that errdefer unreachable; is deadcode already. and worse, errdefer unreachable; // deinit the object here is commented out dead code, written in English.

This point has already been addressed. Zig allows, and will allow, an unreachable in explicitly dead code positions. The reason is that if that position becomes reachable, then the code won't compile.

So it's a load-bearing part of the code, because it turns "happens to be unreachable" into "must be unreachable".

Consider:

fn doNumberThing(number: isize) void {
    if (number < 0) {
         doFirstThing(number);
    } else { 
        doSecondThing(number);
    }
    unreachable;
}

By adding the unreachable, if the else clause is changed into a test, then we have a compile error, not a function which now silently does nothing when it's supposed to always do something.

That's the purpose of errdefer unreachable;, it both documents and guarantees that in the event that there are error conditions to handle, the code won't compile unless and until those error conditions are handled. Whereas an errdefer of any other sort in a function which doesn't throw errors is dead code which can silently become active, with no guarantees that it does the correct thing in the event that happens. As @nmichaels did a fine job of illustrating.

@andrewrk andrewrk removed this from the 0.14.0 milestone Jan 25, 2025
@andrewrk andrewrk added this to the 0.15.0 milestone Jan 25, 2025
# for free to join this conversation on GitHub. Already have an account? # to comment
Labels
accepted This proposal is planned. proposal This issue suggests modifications. If it also has the "accepted" label then it is planned.
Projects
None yet
Development

No branches or pull requests

7 participants