-
-
Notifications
You must be signed in to change notification settings - Fork 3.1k
Description
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, and
try + 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:
- Multiple statements may reference a single error handler,
- a handler may appear anywhere in the function after the statement(s) that reference it,
- triggering a handler skips any statements between the trigger point and the handler, and
- execution continues after the handler.
const f = fn(iPath []const u8) !void { // EDIT: was Go, now Zig
var buf: [1024]u8 = undefined;
const file|eos| = std.fs.openFileAbsolute(iPath, .{}); // path may not exist
defer file.close();
|eos| = file.seekTo(42);
const len|eos| = file.read(buf);
|epr| = process(buf[0..len]); // skip this on OS error
catch eos switch (eos) { // handle OS error
error.FileNotFound => |epr| = process(null),
else => @panic(...),
}
catch epr { // handle processing error
std.log.println(epr);
return;
}
finish(); // called unless catch returns
}
Zig's if (...) |val| {...} else |err| {...} syntax can get noisy:
if (f()) |val| {
one(val);
if (g()) |val| {
two(val);
} else |err| {
fixtwo(err);
}
} else |err| {
fixone(err);
}
With catch this could be either of:
const val|e1| = f(); // maybe just: val | e1
one(val);
val|e2| = g();
two(val);
catch e2 fixtwo(e2);
catch e1 fixone(e1);
one(_|e1| = f());
two(_|e2| = g());
catch e2 fixtwo(e2);
catch e1 fixone(e1);
And catch could infer the error from the preceding statement:
const val|err| = f();
catch { log(err); => 1; } // inits val to 1
const val|_| = f();
catch { => 1 } // inits val to 1
When error var is omitted, catch is an operator like today:
const val = f() catch 1;
For any operation that yields an error which is not caught, returning an error could be implicit. That makes try unnecessary.
fn f() !void {
const val|_| = g(); // if g() returns error, f() returns it
|_| = h();
h(); // terse variants
const val = g();
}
We would want some built-in handler names for common cases:
|@panic| = f() // panic on error
|@log| = f() // log error and return it
|@logc| = f() // log error and continue
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.