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
Overhaul error handling control-flow and add error-stack intent #7812
Comments
Error unions as values are one of the things that makes error handling in zig feel "simple", I would be loathe to give it up. Furthermore, Aside, I'm not a fan of the proposed |
Can you show me idiomatic usage of this? Where are error-unions used as values? As far as I know, error-union tags are not easily accessed leaving only prescribed control-flow mechanics. Can use
In all of Lines 1249 to 1265 in 8118336
It is such a rarity. Why subject folks to wall of error messages when a simple
|
The "consider using try…" message will always be at the top of the stack of errors. There is another issue open for improving how error messages are printed so it is easier to find the top of the error list because right now every error is buried at the top of a long list. This proposal has no effect on how long the list of error messages is when ignoring an error (for example the error for |
untrue, Zig has no hidden flow control. But zig does hide error handling
yes, one error is swapped for another. edit: but in all cases where we have excessive stack depth caused by lack-of-reset this proposal will yield shorter error lists. |
@mikdusan Sure, you can name your error const AllFine = error {
AccessDenied,
OutOfMemory,
FileNotFound,
}; or const allfine = foo(AllocationError.OutOfMemory);
std.testing.expect(allfine == FileOpenError.OutOfMemory); Thats the tradeoff zig made in simplicity. The alternative is to handle things like Rust with Result and Option, which are required to be explicitly defined. |
@matu3ba, re: const allfine = foo(AllocationError.OutOfMemory);
std.testing.expect(allfine == FileOpenError.OutOfMemory); I am not understanding your point. That code should work unchanged with this proposal because it is only dealing with error-set. This proposal is at the error-union level of things. |
i like zig error handling (except the use of error sets - there was a reason C++ moved away from it). i also like the idea here that error returns be treated as exceptional cases, default being to expect results, not errors. the example:
|
const c = catch doit(); // catch prefix: explicitly wanting the whole error union The problem with this is |
this may well be true for the `catch` *postfix* ( `doit() catch ...` ) but
here im proposing a `catch` *prefix* ( `catch doit()` ), using `catch`
instead of `notry`. I personally could not find a more suitable keyword to
describe the intent of grabbing the whole error union.
…On Tue, Feb 9, 2021 at 4:30 PM Michael Dusan ***@***.***> wrote:
const c = catch doit(); // catch prefix: explicitly wanting the whole error union
The problem with this is catch implies error but not error-union, which
is what I'd like to make clear. And if we care, it's also the same with
many (most?) langs that also have keyword catch it only grabs the actual
error thing or exception.
—
You are receiving this because you commented.
Reply to this email directly, view it on GitHub
<#7812 (comment)>, or
unsubscribe
<https://github.com/notifications/unsubscribe-auth/ADAMS37FRUFK5KOOIDHNWQ3S6FBGVANCNFSM4WG6ZLLQ>
.
|
Just want to say I really like the concepts being proposed here. This is making Zig easier to use correctly and harder to use incorrectly. Zig's error handling semantics are already very slick, but they do have a few minor idiosyncrasies that I think this proposal nicely addresses. Could the proposed Obligatory Bikeshed: Not sure I'm stoked on the keyword |
For those reading the comments, all comments up to here were for "Revision 1". At this point in time, the proposal is now at Revision 3. |
I use that to write wrappers on the functions that can error. Example from my code: fn read(self: *@This(), index: usize, old_perms: Permissions, new_perms: Permissions) !ObjectRef {
/*...*/
}
pub fn read_from_consumer(self: *@This(), index: usize) !ObjectRef {
return self.read(index, .ToBeReadByConsumer, .OwnedByConsumer);
}
pub fn read_from_producer(self: *@This(), index: usize) !ObjectRef {
return self.read(index, .GrantedReadRights, .OwnedByConsumer);
} |
@notYuriy by that I meant "put into a variable" and something more than just |
@mikdusan Looks like a good proposal now. Some suggestions to improve readability and understandability from my side:
throw error{ .InvalidChar = index };
throw ParseError{ .InvalidChar = index };` looks rather ugly. EDIT: 5 is not quite true, as it depends on the entry and exit paths of catch and throw and how they change the available error set for the followup operations in the control-flow. However, I hope you get abit the idea what I mean with that. |
One thing I can't really tell from this proposal is who is responsible for the memory of the propagated value. |
@Luukdegram this proposal doesn't seek to change that an error value is a primitive -- large enough to give a unique value to all possible error "enums". |
@mikdusan I see, thanks for clarifying that. |
I really like this proposal in some ways, but I'm unclear on what exactly the rules are for prefix // required for function call when assigning
const a = catch returnsErrorUnion(); // required
// what about if an explicit type is given?
const b: Error!usize = returnsErrorUnion();
// required when forwarding a temporary?
const c = eatError(returnsErrorUnion());
// needed for a copy?
const d = a;
const e = Type.errorUnionDecl;
const f = inst.errorUnionField;
const g = optionalErrorUnion.?;
const h = ptrToErrorUnion.*;
// needed for complex expression?
const i = for (list) |item| {
if (isAcceptable(item)) break item;
} else error.NotFound;
// what about generics?
const T = Error!usize;
const j = undefinedInstance(T);
// or when storing into a field?
inst.errorUnionField = returnsErrorUnion();
errorUnionArray[idx] = returnsErrorUnion();
// and in tuples?
print("{}\n", .{ returnsErrorUnion() }); |
@SpexGuy, added inline comments beginning with ALLCAPS word: // required for function call when assigning
const a = catch returnsErrorUnion(); // required
// what about if an explicit type is given?
// ERROR: unhandled error
const b: Error!usize = returnsErrorUnion();
// OK
const b: Error!usize = catch returnsErrorUnion();
// required when forwarding a temporary?
// ERROR: unhandled error
const c = eatError(returnsErrorUnion());
// OK
const c = eatError(catch returnsErrorUnion());
// needed for a copy?
// OK: not a function call
const d = a;
// OK: @typeOf(Type.errorUnionDecl) != error-union and not a function call
const e = Type.errorUnionDecl;
// OK: not a function call
const f = inst.errorUnionField;
// OK: not a function call
const g = optionalErrorUnion.?;
// OK: not a function call
const h = ptrToErrorUnion.*;
// needed for complex expression?
// OK: not a function call
const i = for (list) |item| {
if (isAcceptable(item)) break item;
} else error.NotFound;
// what about generics?
// OK: @typeOf(Error!usize) != error-union and not a function call
const T = Error!usize;
// ERROR: unhandled error; if we want to shuttle around return values like that, it needs to be boxed
const j = undefinedInstance(T);
// or when storing into a field?
// ERROR: unhandled error
inst.errorUnionField = returnsErrorUnion();
// ERROR: unhandled error
errorUnionArray[idx] = returnsErrorUnion();
// and in tuples?
// ERROR: unhandled error
print("{}\n", .{ returnsErrorUnion() });
see my next comment |
Here I think are 2 rules that work:
var eu = catch doit();
var eu = catch doit();
return try eu;
return try doit(); |
Instead of fn trace_errors(f: fn() !void) !void {
f() catch |err| {
std.debug.warn("error traced: {}\n", .{err});
throw;
}
} C# has similar concept |
C++ and Ruby also have similar keyword-without-arg to re-throw/re-raise. Definitely worth considering imo. |
Some counter points to For example, you may have:
Error as values is a really nice concept and making it harder to work with errors directly make it more painful. |
Just FYI, I believe your example is equivalent to this one line: return writeToDisk() catch |err| (mitigateError(err) orelse err); |
When it is simple, sure, but it is usually not just a single call, at which point I can either use |
REVISION 3 - Friday 2021-02-12
note: changelog is at bottom of document
note: co-author @marler8997
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 changesreturn error.BadValue;
withthrow error.BadValue;
secondary (and orthogonal) to but included in this proposal:
@throwCurrentErrorHideCurrentFrame()
to throw without pushing error/frame onto error-stack@clearErrorStack()
for explicit control of when to clear the error-stack.union(enum) { payload: T, err: E }
Related proposals:
#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.
pros
cons
throw
catch
to work as a prefix keywordcatch <error-union>
example.zig
with this proposal in mindtable of fundamental error-handling effects and corresponding syntax
throw
→ error control-flow and exits functionpush
→ pushing one (or more) errors onto error-stackclear
→ clear error-stack (length becomes 0)const x = catch doit();
const x = catch doit(); @clearErrorStack();
@throwCurrentErrorHideCurrentFrame();
const result_value = try doit();
throw error.BadValue;
@clearErrorStack(); throw error.BadValue;
Questions and Answers
Q. Why a new keyword
throw
only to handle an expression of error-type. Could existing keywordtry
be overloaded?A. The
try
keyword means "maybe throw" and overloading it to accept expressions of typeerror
would change that use to meanalways throw
. The semantics of try would depend on the value of the expression after it and its no longer clear whattry 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".
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 #1923 (comment) . Also this is relatively rare comparing to common usage of error flow-control and we again decided against promotion to a keyword.
What #2647 syntax would look like under this proposal:
REVISION 3 changes
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.
@clearErrorStack()
and@throwCurrentErrorHideCurrentFrame()
failagain
throw
instead of overloadingtry
fail
tothrow
REVISION 2 changes
notry
with overload ofcatch
continue
with overload of keywordtry
raise
withfail
because it feels less like exceptionsThe text was updated successfully, but these errors were encountered: