pqsrc
May 27, 2025, 4:18pm
1
I need this all the time and I’m aware of the current recommended pattern:
(err: {
const foo = run() catch |err| break :err err;
const bar = func(foo) catch |err| break :err err;
return bar.value;
}) catch |err| {
// ...
};
but it’s quite wordy and difficult to read and my code always ends up running off the right side of the screen. Has anyone proposed this yet?
(err: {
const foo = try :err run();
const bar = try :err func(foo);
return bar.value;
}) catch |err| {
// ...
};
Why was it rejected?
I’d like to see regular try blocks, but I guess they are even more anathema than your proposal E.g.
try {
const foo = run();
const bar = func(foo);
return bar.value;
} catch |err| {
// ...
}
…e.g. this would be syntax sugar for putting a try
in front of each function that has an error union return value.
I might miss some subtle incompatibilities with the Zig syntax or philosophy though
2 Likes
not for nothing, but i think the Zig Zen way is just
fn fallible() !Value {
const foo = try run();
const bar = try func(foo);
return bar.value;
}
// ...
const value = fallible() catch |err| {
// ...
};
// ...
7 Likes
I think this pattern isn’t compatible with current syntax. A {}
block is a statement that evaluates to something and I don’t see what this something would be here. try
is used to unwrap an error union to a value or return, but if the error union is already unwrapped what does the catch
catch?
What a try block is supposed to express also isn’t equivalent to this:
const foo = try run();
const bar = try func(foo);
return bar.value;
which wouldn’t give the user a chance to handle err
in place and would just return it to the caller, but rather to this:
const foo = run() catch |err| handle(err);
const bar = func(foo) catch |err| handle(err);
return bar.value;
which explicitly doesn’t use try
anywhere because it doesn’t just return err
from the function and handle()
s it some other way. So I think using try {}
here doesn’t really make sense, it implies that err
is just returned.
I really like this suggestion, the ability to give try
a scope would be very useful I think. Also it makes more sense syntactically IMO.
Although maybe there’s more value in keeping return
and try
which deal with functions separate from break
, continue
etc. which deal with blocks.
Relevant issue:
opened 04:10PM - 15 Jun 20 UTC
proposal
## Motivation
Consider the following example from my code:
```zig
fn addA… rgument(wl_client: ?*c.wl_client, wl_resource: ?*c.wl_resource, arg: ?[*:0]u8) callconv(.C) void {
const self = @ptrCast(*Self, @alignCast(@alignOf(*Self), c.wl_resource_get_user_data(wl_resource)));
const id = c.wl_resource_get_id(wl_resource);
const allocator = self.server.allocator;
const owned_slice = std.mem.dupe(allocator, u8, std.mem.span(arg.?)) catch {
c.wl_client_post_no_memory(wl_client);
return;
};
self.args_map.get(id).?.value.append(owned_slice) catch {
c.wl_client_post_no_memory(wl_client);
allocator.free(owned_slice);
return;
};
}
```
The error handling here requires code duplication if I want to keep things in one function. I can't preform the error handling at the callsite either since this function is a callback being passed to a C library. The amount of code being duplicated for error handling is not too large in this case, but it is easy to imagine a case in which it could grow larger. My only option to avoid this code duplication is to create a helper function:
```zig
fn addArgumentHelper(wl_client: ?*c.wl_client, wl_resource: ?*c.wl_resource, arg: ?[*:0]u8) !void {
const self = @ptrCast(*Self, @alignCast(@alignOf(*Self), c.wl_resource_get_user_data(wl_resource)));
const id = c.wl_resource_get_id(wl_resource);
const allocator = self.server.allocator;
const owned_slice = try std.mem.dupe(allocator, u8, std.mem.span(arg.?));
errdefer allocator.free(owned_slice);
try self.args_map.get(id).?.value.append(owned_slice);
}
fn addArgument(wl_client: ?*c.wl_client, wl_resource: ?*c.wl_resource, arg: ?[*:0]u8) callconv(.C) void {
addArgumentHelper(wl_client, wl_resource, arg) catch {
c.wl_client_post_no_memory(wl_client);
return;
};
}
```
Doing this for every callback I wish to register gets annoying and feels like unnecessary boilerplate.
## Proposal
**EDIT: the original proposal isn't great, see @SpexGuy's [comment](https://github.com/ziglang/zig/issues/5610#issuecomment-644272436) for the much better revised version.**
To summarize the revised proposal:
1. Fix peer type resolution so it correctly infers error union block types if a block is broken with both an error and another value.
2. Modify the semantics of `errdefer` to mean "runs if the containing block yields an error to any outside block". Both returning and breaking with error count as yielding an error to an outside block, as long as the evaluated type of the outside block is an error union. (this is to stay consistent with the current behavior if a function returns an error value but not an error union).
3. Introduce the keyword `try_local`, used as `try_local :blk foo();` which is sugar for
`foo() catch |e| break :blk e;`. As mentioned in a comment below, the `try` keyword could instead be overloaded to have the described behavior if given a label as in `try :blk foo()`.
I think both 1 and 2 are unambiguously good changes. The addition of a new keyword for 3. may not be worth it, though it would certainly be welcome in the examples I've presented here.
With the revised proposal, the above example would look like this:
```zig
fn addArgument(wl_client: ?*c.wl_client, wl_resource: ?*c.wl_resource, arg: ?[*:0]u8) callconv(.C) void {
const self = @ptrCast(*Self, @alignCast(@alignOf(*Self), c.wl_resource_get_user_data(wl_resource)));
const id = c.wl_resource_get_id(wl_resource);
const allocator = self.server.allocator;
blk: {
const owned_slice = try :blk std.mem.dupe(allocator, u8, std.mem.span(arg.?));
errdefer allocator.free(owned_slice);
try :blk self.args_map.get(id).?.value.append(owned_slice);
} catch {
c.wl_client_post_no_memory(wl_client);
return;
};
}
```
<details>
<summary> Original proposal</summary>
Doing this for every callback I wish to register gets annoying and feels like unnecessary boilerplate. As an alternative, I propose allowing `catch` to be used on blocks, which would allow expressing this control flow like so:
```zig
fn addArgument(wl_client: ?*c.wl_client, wl_resource: ?*c.wl_resource, arg: ?[*:0]u8) callconv(.C) void {
const self = @ptrCast(*Self, @alignCast(@alignOf(*Self), c.wl_resource_get_user_data(wl_resource)));
const id = c.wl_resource_get_id(wl_resource);
const allocator = self.server.allocator;
{
const owned_slice = try std.mem.dupe(allocator, u8, std.mem.span(arg.?));
try self.args_map.get(id).?.value.append(owned_slice);
} catch {
c.wl_client_post_no_memory(wl_client);
return;
};
}
```
Catch blocks have simple rules:
```zig
fn add(a: i32, b: i32) !i32 {
return error.CantMath;
}
fn div(a: i32, b: i32) !i32 {
if (b == 0) return error.DivZero;
return error.CantMath;
}
fn sub1(a: i32) !i32 {
if (a <= 0) return error.Oops;
return a - 1;
}
pub fn main() !void {
const a = 1;
const b = 5;
// The catch expression must evaluate to the same type as the block
const out = blk: {
const c = try add(a, b);
break :blk try div(a, c);
} catch |err| switch (err) {
error.CantMath => 42,
error.DivZero => "thing", // compile error
};
// Otherwise it must be noreturn
const out = blk: {
break :blk try add(a, b);
} catch return;
// catch blocks may be nested, errors handled with try are always caught
// by the innermost catch block, or returned from the function if not inside
// a catch block.
{
const aa = try sub1(a); // caught by outer handler
const aaa = blk: {
break :blk try sub1(aa); // caught by inner handler
} catch |err| {
std.debug.warn("inner handler: {}", .{err});
};
} catch |err| {
std.debug.warn("outer handler: {}", .{err});
};
bb = try sub1(b); // error is returned from main()
}
```
Consider this an alternative to #5421 solving some of the same issues in a way that is in my opinion more consistent with the rest of the language.
## Drawbacks
The major disadvantage to this proposal is that it changes the behavior of the `try` keyword based on the scope it is used in. `try` is currently syntactic sugar for `catch |err| return err;` and changing this complicates things. However I think the proposed semantics of `try` are straightforward enough.
</details>
2 Likes
Raku CATCH
block syntax would be even more clearer:
{
a();
catch |err| {
c();
}
try b();
}
Which, in most other programming languages, translates to:
{
a();
try {
b():
} catch |err| {
c();
}
}
Proposal: introduce errbreak
Works just like break
but only executes if an error occurs, analogous to defer
and errdefer
.
It would only be usable inside a block of any kind as an alternative to try
and would unwrap error unions.
Instead of:
blk: {
var foo = bar() catch |err| break :blk err;
foo.baz() catch |err| break :blk err;
return foo;
} catch |err| {...}
you could write:
blk: {
var foo = errbreak :blk bar();
errbreak :blk foo.baz();
return foo;
} catch |err| {...}
This is similiar to try :blk
from this thread and try_local
from #5610 but I think the name is more clear.
pqsrc
May 27, 2025, 8:03pm
7
Thanks for the link! I see my idea was already proposed here and not officially rejected, but it’s been 5 years of silence so I don’t have a lot of hope.
That first block is perfectly valid in current Zig, isn’t it? Honestly I haven’t found myself wishing I had a block catch yet, because I usually just try
a bunch of different function calls, and then catch
+switch
at the call site if necessary to handle it all. If I wanted to do immediate error handling of a block like this though, this seems like a reasonable solution using existing syntax.
Actually it doesn’t compile because zig can’t infer the type of blk
I think but I’ve used this pattern in the past to implement C callbacks where the only error handling I could really do was logging the error and returning 1 like this:
[...]
const a = blk: {
const b = foo() catch |err| break :blk err;
[...]
break :blk b;
} catch |err| {
log.err("{s}\n", .{@errorName(err)});
return 1;
};
[...]
Something like errbreak
or whatever would save a couple of keystrokes here. But using this pattern is certainly already possible with existing syntax.