-
-
Notifications
You must be signed in to change notification settings - Fork 2.8k
Proposal: remove capturing errdefer
from the language
#23734
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Comments
The errdefer block should not be able to influence the result type of the function, right? So technically there shouldn't be any dependency loop here. In any case, this should be the same case as a regular recursive function. |
Could there be a better name than fn unsafeProcessOneTarget() !void {
...
}
fn processOneTarget() void {
...
} I chose It's not directly related to the logic of the proposal, but it might help with selling the idea of creating failable / safe variations of functions |
@haze Golang has the convention "Must" for functions that panic, and I think it's pretty reasonable when there is no "panic in the type system": fn processOneTarget() !void { ... }
fn mustProcessOneTarget() void { ... } |
Well the errdefer could panic or call a noreturn function in which case that error wouldn't be returned, but I think it quickly becomes weird or impossible to alter the inferred error set in response to that. |
It's the opposite of safe if it "handles" errors by panicking! Edge cases matter and not returning the error to the caller is harmful if anything, |
in this case |
It may be useful to note that if #2765 gets added then this behaviour could be replicated as so: errdefer if(@result()) |_| unreachable else |err| {
// do stuff with err
} |
Well yeah the whole idea of this issue is it's not necessary. I'm just saying how the inference could technically be influenced (but it almost certainly shouldn't be influenced like that because that's not what defer/errdefer are for). |
For another datapoint on large projects, Bun contains 257 errdefers but no capturing ones. |
Is this specifically for capturing the err value from the |
For capturing the err value. |
As I explained in a ziggit discussion , there are situations where the resource clean-up procedure is slightly different after an error has occurred. Here's one example: fn runQuery(allocator: std.mem.Allocator, sql: []const u8) ![]Row {
const conn = connection_pool.get();
defer |err_maybe| {
// do no reuse connection when a connection has timed out
connection_pool.release(conn, err_maybe == error.Timeout);
}
// ...
} That we cannot capture error in a fn runQuery(sql: []const u8) !RowIterator {
const conn = connection_pool.get();
errdefer |err| {
// do no reuse connection when a connection has timed out
connection_pool.release(conn, err == error.Timeout);
}
// ...
} Since we would only ever care about specific errors related to the resource being cleaned up, the problem described here concerning switch is largely irrelevant. |
Edit: The in #5610 proposed syntax would add another option to replace Original commentMaybe the capturing This would mean that the expressions try :blk error_union and error_union catch |err| break :blk err would be equivalent. Code could look like this: const std = @import("std");
fn doSomething() !u64 {
// do something
return error.Foo;
}
fn foo() void {
const num = blk: {
const inner_num = try :blk doSomething();
comptime std.debug.assert(@TypeOf(inner_num) == u64);
break :blk doSomething();
} catch |err| switch (err) {
error.Foo => std.posix.exit(1),
};
comptime std.debug.assert(@TypeOf(num) == u64);
} |
Background
defer
is a core and incredibly useful component of Zig, which helps avoid bugs in resource management. Similarly,errdefer
helps avoid bugs in situations where a resource should be cleaned up only on error conditions. However, there is a third, lesser-known, piece of syntax: capturingerrdefer
. It looks like this:This feature is used incredibly rarely; many experienced Zig users do not even know that it exists. As a data point, in the Zig repository, ignoring test coverage and documentation for it, there are exactly two uses of this language feature:
zig/tools/update_cpu_features.zig
Line 1529 in 8e79fc6
zig/lib/fuzzer.zig
Line 298 in 8e79fc6
A feature being rarely used is not necessarily in itself a reason to remove that feature. However,
errdefer
has a bigger design problem.The Problem
Here's a question: in
errdefer |err|
, what is the type oferr
?The obvious thing would be that it has the type of "every error which can be returned after this point in the function", but this isn't a feasible definition; it brings a good amount of (quite boring) complexity to the language specification in terms of how it interacts with things like inferred error sets, and implementing this would require a type of logic in the compiler which likely has compiler performance implications. So, in reality, there are 3 reasonable choices:
err
is the current function's error set.err
isanyerror
.err
is the error type which is being returned at a givenreturn
site (so theerrdefer
body is reanalyzed at every possible error return with a different type forerr
).Let's go through these three options. Note that right now, the compiler has inconsistent and unhelpful behavior here, so this is an unsolved problem.
The first option is actually fairly good, with two big caveats:
errdefer |err| switch (err)
becomes impossible (it would emit a dependency loop because we don't know what cases need to be in theswitch
).errdefer |err| fatal("error: {}", .{err});
impossible, since the function needs to return an error forerr
to be typed correctly.The second option means that any switch on the captured
err
must have anelse
prong, so if you want toswitch
on the captured error, this option is strictly worse than using a wrapper function whichswitch
es on the returned error (since at this point the error type is known and can be exhaustively switched on). However, people are likely to reach forerrdefer
anyway out of convenience, and shoot themselves in the foot by losing type safety.The third option is what we usually do today, but:
switch
on the captured error, because@TypeOf(err)
will usually only contain a subset of all possible errors, so you'll get errors thatswitch
prongs are impossible (because e.g.error.Foo
can't be returned at this particular return site, soerr
iserror{Bar}
instead).errdefer
s across different error return sites, because they are analyzed differently. (This Zig implementation does not do this deduplication anyway, but it's good for the language spec to make it viable!)One observation here is that all three of these solutions have big problems with
switch
on the captured error -- and this is a construct we want to encourage, not discourage! The main other use case for capturingerrdefer
is to log errors. The thing is, this use case is actually not a brilliant use for this construct, because it can lead to code bloat (due to the error logging logic being duplicated at every error return site). Generally, the body of anerrdefer
should be incredibly simple; it's essentially just intended for resource cleanup. Capturingerrdefer
encourages using the construct in more complex ways, which is usually not a good thing! Also, if you're logging an error, this generally indicates you aren't going to explicitly handle the different error cases higher on the call stack, so it's probably desirable to "collapse" these errors down into one (e.g. turn your set of 5 errors into a blanketerror.FooFailed
). This is somethingerrdefer |err|
does not support. As such, there are several big advantages to using a "wrapper function" approach instead, like this:So, capturing
errdefer
syntax raises a difficult design problem, where all solutions seem unsatisfactory. Furthermore, the common use cases for this feature -- and indeed, the ones it seems to encourage -- tend to emit bloated code and suffer from worse error sets. That leads on to this proposal.Proposal
Remove
errdefer |err|
syntax from Zig. Uh... yeah, that's kinda it.Migration
The two uses in the compiler can be trivially rewritten with these diffs.
lib/fuzzer.zig
tools/update_cpu_features.zig
These are also basically the two solutions for current uses of this feature: either split your function in two, or perhaps use
catch
at the error site(s) if there aren't many. This improves clarity (one less language feature to understand in order to understand your code!), type safety (no issues withswitch
exhaustivity, and you can "collapse" error sets where you want as discussed above), and code bloat (no duplication of the error handling path at every possible error return).The text was updated successfully, but these errors were encountered: