-
-
Notifications
You must be signed in to change notification settings - Fork 2.3k
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
let 'catch' define named error handlers #5421
Comments
How does this actually change the language? Typical error handling in Zig looks like this: const foo = bar() catch |err| switch (err) {
error.A => {},
else => {},
}; What would this look like with your proposal? |
In the proposal, These are present in the first example above. I've added this list to the text. @pixelherodev, your example is for a handler bound to a single statement with no intermediate statements, so it would look much the same. |
you can already get fairly close to this with #1717 which is accepted: const std = @import("std");
const explode = fn() !void {
return error.Boom;
}
const sleep = fn() !void {
return error.CantSleep;
}
const run = fn() !void {
return error.Tripped;
}
pub const main = fn() void {
const blk = fn() !void {
try explode();
try sleep();
try run();
}
blk() catch |err| switch (err) {
...
};
} |
Seems very similar to handling errors using |
I agree, this is literally goto with a different name. We have |
While this is mostly true, it should be noted that this proposal only allows forward jumps, so the potential for spaghetti code is reduced significantly.
Not quite. The whole point of this proposal (if I understand it correctly), is to simplify the direct handling of multiple instances of multiple failure modes within a single function. The That said, I do think that this proposal would make control flow sufficiently confusing that the potential benefits are probably not worth it. |
You're right, the semantics of the proposed syntax are slightly different than goto. However, I think they have the potential to be just as hard to follow in terms of control flow.
and a more idiomatic version of the "status quo" snippet: const val = f() catch |err| {
fixone(err);
return;
};
one(val);
const val2 = g() catch |err| {
fixtwo(err);
return;
};
two(val2); The control flow in the second example is in my opinion far easier to understand without becoming cumbersome. I think that goto-esque jumps in control flow violate the
|
Isaac, I believe you added returns which don't appear in my code. If you change them to goto and add a label, then it would be the same ;-) Agreed that this, and traditional exceptions, entail an implicit goto. But so do You could indent my second example to indicate what code is skipped on error (altho that doesn't work for my first example, where the first catch can invoke the second):
|
ok, perhaps this is closer to the control flow you intended? const val = f() catch |err| fixone(err);
one(val);
const val2 = g() catch |err| fixtwo(err);
two(val2); if not, my question become how does the program continue after the catch statements if it doesn't return and val/val2 are not initialized? |val|e1| = f(); // maybe just: val | e1
one(val);
|val|e2| = g();
two(val);
catch e2 fixtwo(e2);
catch e1 fixone(e1);
// I guess execution continues here but what is done if we errored? If |
It's a contrived example to demonstrate the noise of |
Here's a zig version of your first example in go. I'm not too familiar with go so feel free to correct me if i've gotten something wrong. This also depends on #1717 const main = fn() !void {
var buf: [1024]u8 = undefined;
const readToBuf = fn (buf: []u8) ![]u8 {
const file = try std.fs.openFileAbsolute("/path/to/file", .{});
defer file.close();
try file.seekTo(42);
const len = try file.read(buf);
return buf[0..len];
};
const maybe_slice = readToBuf(&buf) catch |err| switch (err) {
error.FileNotFound => null,
else => return err,
};
try process(maybe_slice);
finish();
} Edit: fixed some mistakes |
Thanks! I rev'd the issue to use Zig, plus new syntax for Re the use of nested functions as a sort of |
Here's my take on translating the example into status quo Zig that compiles & runs: const std = @import("std");
fn f(path: []const u8) !void {
var buf: [1024]u8 = undefined;
var len: usize = 0;
if (std.fs.cwd().openFile(path, .{})) |file| {
defer file.close();
file.seekTo(42) catch unreachable;
len = file.read(&buf) catch unreachable;
} else |err| switch(err) {
error.FileNotFound => {}, // len will be 0
else => unreachable,
}
process(buf[0..len]) catch |err| {
std.debug.warn("{}\n", .{err});
return;
};
finish();
}
pub fn main() !void {
try f("file");
try f("missing");
try f("filez");
}
fn process(bytes: []u8) !void {
std.debug.warn("processing: \"", .{});
var has_z: bool = false;
for (bytes) |byte| {
if (byte == 'z') {
has_z = true;
}
std.debug.warn("{c}", .{byte});
}
std.debug.warn("\"\n", .{});
if (has_z) {
return error.NoZAllowed;
}
}
fn finish() void {
std.debug.warn("finish called\n", .{});
}
Output is:
The thing that makes this example strange is the abundance of If the possible errors from |
Ryan, thanks for your analysis. Note that I made my examples trivial for the sake of clarity. Regarding And most application developers today are familiar with exceptions, which help keep the happy path cohesive and minimally indented. Thinking "about all three functions' failure modes at the same time" can be an advantage -- you share what's common and
|
I'm just some guy, so I might be wrong about this, but as I understand it, Zig actively wants to change this. See the Errors section of this talk. and especially this part talking about Again, as I understand it, About shared error handlers: just to be sure we're on the same page, note that the following code will fail to compile: const std = @import("std");
pub fn main() !void {
const file = std.fs.cwd().openFile("file", .{}) catch |err| switch(err) {
error.FileNotFound => return err,
else => unreachable,
};
defer file.close();
// copy+pasted error handling from openFile
file.seekTo(42) catch |err| switch(err) {
error.FileNotFound => return err,
else => unreachable,
};
}
This behavior doesn't seem congruent with the 'shared error handlers' part of the proposal. |
Ok, a catch handler would have to be able to switch on the errorset, and then the error. Aborting when you know the program is wrong, or the system under you is hosed, is Good Practice, IMO. That doesn't prevent the user or watchdog from restarting immediately. EDIT: I misused |
Unless I'm missing something, that would mostly defeat the purpose of shared error handling (or it would have the same effect as enforcing that labeled error handlers are specific to a single error set).
|
Note that you can still handle all the errors at once in status quo zig like so: const std = @import("std");
fn readToBuf(buf: []u8) ![]u8 {
const file = try std.fs.openFileAbsolute("/path/to/file", .{});
defer file.close();
try file.seekTo(42);
const len = try file.read(buf);
return buf[0..len];
}
fn main() !void {
var buf: [1024]u8 = undefined;
const maybe_slice = readToBuf(&buf) catch |err| switch (err) {
error.FileNotFound => null,
else => return err,
};
try process(maybe_slice);
finish();
} The only difference is that without #1717 you must define the |
I inserted two paragraphs in the issue text #5421 (comment), starting with: The goal here is clear error handling inside complex business logic... |
I've extended the proposal with items for uncaught errors and built-in handler names. |
I realized you can also use
|
I'm the author of golang/go#27519 -- one of very few Go error handling proposals to remain open, after numerous other ideas to improve Go's error handling have been debated and discarded.
Since Zig is close to finalizing its language spec, I'd like to throw this into the ring. I've been watching Zig with interest from a distance. I'm disappointed in Go as a language, altho its tools, runtime, and stdlib are quite good.
The goal here is clear error handling inside complex business logic with a myriad of recovery schemes, some of which are shared. The construct below may seem alien to you at first sight, but it's completely natural to zillions of folks writing catch blocks in Java, Python, and Javascript. (And sure, you can approximate it with a nested function, but that is both alien to this constituency and heavy on boilerplate.)
In Zig today,
f() catch |err| switch (err) {...}
can't skip code on error, a standard feature of catch,if ... else |err| {...}
skips code like standard try/catch, but yields a noisy indent cascade, andtry
+errdefer
is a case of catch (!) that throws unrecoverable (?) errors to the caller.I respectfully submit that these are not the ideal semantics for a next-gen C.
Instead, let
catch
define named function-local error handlers. Unlike exceptions, this does not propagate errors up the stack, and ties a statement that can yield an error to a specific handler. It offers these features:Zig's
if (...) |val| {...} else |err| {...}
syntax can get noisy:With
catch
this could be either of:And
catch
could infer the error from the preceding statement:When error var is omitted, catch is an operator like today:
For any operation that yields an error which is not caught, returning an error could be implicit. That makes
try
unnecessary.We would want some built-in handler names for common cases:
There's a lot of detail in golang/go#27519 and the requirements doc linked from it. If there's interest, I can work with folks here to massage that material into a Zig proposal.
The text was updated successfully, but these errors were encountered: