The primary goal of this proposal is to lexically distinguish error control-flow from regular flow-control:
return
statements do not effect error flow-control or error-stack changes- replace current
return error.BadValue;
withthrow error.BadValue;
- require explicit syntax to capture an error-union value
secondary (and orthogonal) to but included in this proposal:
- add new builtin
@throwCurrentErrorHideCurrentFrame()
to throw without pushing error/frame onto error-stack - add new builtin
@clearErrorStack()
for explicit control of when to clear the error-stack. - expose error-union tags as if
union(enum) { payload: T, err: E }
ziglang/zig#1923 (comment) overlaps with @clearErrorStack()
and opinions related to #1923 are encouraged.
#2562 is no longer required because return <value>
is not ambiguous with error handling.
#2647 is proposing syntax changes to enable tagged-error-unions; see bottom of document for more details.
#5610 is orthogonal to this proposal.
- no more ambiguity between error and value semantics
- common case is to avoid error semantics unless explicit lexical syntax is used
- simplified compiler errors for common cases
- lexical distinction that an error-union value has been taken
- lexical distinction when/where an error-stack has been cleared
- allow inspection and propagation of unchanged error-stack
- new keyword
throw
- overload
catch
to work as a prefix keywordcatch <error-union>
- requires rare source code changes for decls of error-union types (eg. 16 instances identified by std.zig test coverage)
- requires source code changes for error-union callsites to choose between error or value semantics, most will be error semantics
fn afunc() !(error {Bad}) {
// UNCHANGED-BEHAVIOUR
var a = try doit();
// UNCHANGED-BEHAVIOUR
var b = doit() catch (err) { ... };
// UNCHANGED-BEHAVIOUR
if (try doit()) {}
// UNCHANGED-BEHAVIOUR
if (doit()) {}
else |err| {}
////////////////////////////////////////
// OLD: propagate error
// NEW: return an error value, does not effect error-stack, no error-flow-control.
return error.Bad;
// OLD: n/a
// NEW: propagate error (equivalent to OLD `return error.Bad`)
throw error.Bad;
// OLD: `c` is type error-union
// NEW: do not let an error-union _easily_ become a value. compile-error: unhandled error
var c = doit();
// OLD: n/a
// NEW: let an error-union become a value with overloaded keyword `catch` in a prefix position, `d` type is error-union
var d = catch doit();
// exposed union tags
_ = d.payload; // safety checked
_ = d.err; // safety checked
// UNCHANGED-BEHAVIOUR
if (d) {}
else |err| {}
// OLD: compile-error: expected type 'bool', found '@typeInfo(@typeInfo(@TypeOf(foo)).Fn.return_type.?).ErrorUnion.error_set!void'
// NEW: compile-error: unhandled error
if (doit()) {}
}
fn f0() !void {
var a = doit() catch (err) {
// UNCHANGED-BEHAVIOUR but used to be `return` and now `throw`
//
// EXIT-ERROR-STACK: push,propagate
// [1] `f0()` error {Overflow,Megaflow}
// [0] `doit()` error {Overflow,Megaflow}
throw err;
};
// UNCHANGED-BEHAVIOUR
// sugar for above
var a = try doit();
}
fn f1() !u32 {
// error-stack: EMPTY
var a = doit() catch (err) {
// ENTRY-ERROR-STACK:
// [0] `doit()` error {Overflow,Megaflow}
if (cond) {
// UNCHANGED-BEHAVIOUR
// on-error case: push nested error-stack and throw new error
//
// EXIT-ERROR-STACK: push,propagate
// [2] `f1()` error {Overflow,Megaflow}
// [1] `doit()` error {Overflow,Megaflow}
// [0] `doit()` error {Overflow,Megaflow}
try doit();
}
switch (err) {
// NEW-BEHAVIOUR
// case: throw a new error of same value
// it is "new" because error-stack depth has increased
//
// EXIT-ERROR-STACK: push,propagate
// [1] `f1()` error.Overflow
// [0] `doit()` error {Overflow,Megaflow}
.Overflow => throw err,
// NEW-BEHAVIOUR
// case: throw a new error of different value
// it is "new" because error-stack depth has increased
//
// EXIT-ERROR-STACK: push,propagate
// [1] `f1()` error.Unknown
// [0] `doit()` error {Overflow,Megaflow}
.Megaflow => throw error.Unknown,
}
if (cond) {
// NEW-BEHAVIOUR
// case: clear error-stack and throw new error
//
// EXIT-ERROR-STACK: reset,push,propagate
// [0] `f1()` error.BadArgument
@clearErrorStack();
throw error.BadArgument;
}
if (cond) {
// NEW-BEHAVIOUR
// case: clear error-stack and re-throw current-error (top of error-stack)
// if error-stack is empty a runtime error `error.EmptyErrorStack` shall be generated
//
// EXIT-ERROR-STACK: propagate
// [0] `doit()` error {Overflow,Megaflow}
@clearErrorStack();
@throwCurrentErrorHideCurrentFrame();
}
};
// assert( error_stack.len == 0) if (previous catch-block was not entered)
// assert( error_stack.len > 0) if (previous catch-block was entered)
// This particular programmer desires to clear error-stack before any value return.
// Expected to be more common than not clearing, but much less common than try/throw occurrances.
@clearErrorStack();
// assert( error_stack.len == 0)
return 42;
}
// fn return-type: error-union
fn f2() !u32 {
// OLD-BEHAVIOUR
// both accepted; both do same thing
return doit();
return try doit();
// NEW-BEHAVIOUR
// compile-error
return doit();
// ok
return try doit();
// UNCHANGED-BEHAVIOUR
return 42;
}
// fn return-type: value-error-union
// no motivating use case: emergent behaviour
fn f3() @typeInfo(@TypeOf(f2)).Fn.return_type.? {
// NEW-BEHAVIOUR
// let an error-union become value and return value
// error-stack is untouched, no error control-flow, just a regular value being returned
return catch f2();
// desugared
// `veu` is type error-union
const veu = catch f2();
// return value-error-union
return veu;
}
fn f4() !void {
// compile-error: `f3` does not throw
_ = try f3();
// ok: it's just a value-error-union
_ = f3();
}
fn doit() !void {
if (__random__) {
throw error.Overflow;
} else {
throw error.Megaflow;
}
}
throw
→ error control-flow and exits functionpush
→ pushing one (or more) errors onto error-stackclear
→ clear error-stack (length becomes 0)
throw | push | clear | syntax example |
---|---|---|---|
0 | 0 | 0 | const x = catch doit(); |
0 | 0 | 1 | const x = catch doit(); @clearErrorStack(); |
0 | 1 | 0 | invalid - why push onto error-stack if not throwing? |
0 | 1 | 1 | invalid - why push onto error-stack if not throwing? |
1 | 0 | 0 | @throwCurrentErrorHideCurrentFrame(); |
1 | 0 | 1 | invalid - why throw empty error-stack? |
1 | 1 | 0 | const result_value = try doit(); |
throw error.BadValue; |
|||
1 | 1 | 1 | @clearErrorStack(); throw error.BadValue; |
Q. Why a new keyword throw
only to handle an expression of error-type. Could existing keyword try
be overloaded?
A. The try
keyword means "maybe throw" and overloading it to accept expressions of type error
would change that use to mean always throw
. The semantics of try would depend on the value of the expression after it and its no longer clear what try x
does.
Q. Why isn't @throwCurrentErrorHideCurrentFrame()
a keyword and why is it so long?
A. The motivating case for this feature is rare and did not warrant promotion to a keyword. Also we wanted the rare use to be self-explanatory, conveying "a throw of current-error without pushing error/frame onto error-stack".
// this could be made more generic
fn trace_errors(f: fn() !void) !void {
f() catch |err| {
std.debug.warn("error traced: {}\n", .{err});
@throwCurrentErrorHideCurrentFrame();
}
}
Q. Why isn't @clearErrorStack()
a keyword or some other logic engaged to automatically clear?
A. We found multiple cases where a programmer may want to clear the error stack at different places or even not at all. By providing this builtin, the programmer has full control over when to clear the error stack, if at all. Note that this builtin does not prevent other features from also being implemented to clear the error stack such as ziglang/zig#1923 (comment) . Also this is relatively rare comparing to common usage of error flow-control and we again decided against promotion to a keyword.
// syntax from #2647
{
// from comment: https://github.com/ziglang/zig/issues/2647#issuecomment-500694683
return error{ .InvalidChar = index };
// from comment: from https://github.com/ziglang/zig/issues/2647#issuecomment-501545471
return ParseError{ .InvalidChar = index };
}
// syntax under this proposal
{
// error flow-control
throw error{ .InvalidChar = index };
throw ParseError{ .InvalidChar = index };
throw .{ .InvalidChar = index };
// return union-init is regular flow-control
return .{ .age, value };
}
Sorry for the fast update. Rev2 really helped put a lot of things in perspective and with @marler8997's help we brainstormed for a bit and here is the product.
- introduce builtins
@clearErrorStack()
and@throwCurrentErrorHideCurrentFrame()
- and consequently drop new keyword
failagain
- and consequently discontinue block-logic or impacting keywords to clear error-stack
- and consequently use keyword
throw
instead of overloadingtry
- bikeshed: rename
fail
tothrow
- update code samples
- replace new keyword
notry
with overload ofcatch
- replace overload of keyword
continue
with overload of keywordtry
- replace new keyword
raise
withfail
because it feels less like exceptions - clarify how this proposal differs from #1923
- update code samples
see file above