12 ms·
Error Handling in Zig
- nightowl_games 3y agoGood points raised. In Go, I've struggled to attach stack traces to errors. I have a plan (a custom error type, and type checking for that type) but it's not "bulletproof". Seems similar. Seems like there's opportunity for some language evolution here.
- linux2647 3y agoIIRC, part of the language proposal to make errors wrappable in Go’s stdlib included a stack trace. And indeed github.com/pkg/errors includes that. But it never made it into the stdlib implementation
- masklinn 3y ago> Seems similar. It's not: because error sets are a built-in and completely bespoke feature, and they get handled via built-in constructs exclusively (try / catch), zig can actually attach / generate error traces: https://ziglang.org/documentation/0.3.0/#Error-Return-Traces https://ziglang.org/documentation/0.3.0/#Error-Return-Traces
- valenterry 3y agoI find this to be a good example where a language with well defined "foundations" can really shine. If a programming language supports union types and type inference then there is no need for such a "special" language feature as described here. The return type will simply have the form "goodresult | error" and the compiler will infer it based on the function-body alone. At the same time, the following problem of zig is automatically a non-issue: > A serious challenge with Zig's simple approach to errors that our errors are nothing more than enum values. If the result is "goodresult | error" then I can choose of what type "error" is and I'm not bound to any predefined types that the language forces upon me. In fact, the article then shows how to work around the issue with a tagged union. The problem is that this creates another issue: the composition of different error types. E.g. having function foo calling bar and baz and bar can fail with barError and baz can fail with bazError. Then, without having to do anything, the compiler should infer that foo's return type should be "goodresult | barError | bazError". This won't work if tagged unions are used like in the example - they will have to be mapped by hand all the time, creating a lot of noise. While Zig's errorhandling is not bad and miles above Go's, it makes me sad to see that we still lack ergonomy of errorhandling in so many languages despite knowing pretty much all of the language-features that are necessary to make it much easier.
- conaclos 3y agoYou are right. This result of the modeling of tagged unions in terms of enums. Hare [0] avoids this issue by using global tags: every type has a unique global tag. This allows merging tagged unions. [o] https://harelang.org https://harelang.org
- MrJohz 3y agoWouldn't that mean that every type always has to include the type tag as an extra byte, making every type larger overall? Or is it only included if a union type is created from that type?
- conaclos 3y agoNo, the tag is only part of the union. The tag is deterministically and globally computed by the compiler. Tags are encoded in a fixed number of bytes.
- nyberg 3y agoThe issue isn't as simple as just having better error unions with payloads. Constructing the error diagnostics object needs to be taken into account and the costs it brings (e.g does it allocate? take up a lot of stack space? waste resources when one doesn't need the extra information?). Such is a design choice for the programmer not the language as only they know which approach will be more effective given they have a clear view of the surrounding context. An alternative is to allow an additional parameter which optionally points to a diagnostics object to be updated upon error. Returning an error here signals that the object contains additional information while not harming composition as much and giving the choice of where to store such information. This is arguably a better pattern in a language where resource allocation is explicit.
- valenterry 3y agoI think what you are saying is pretty orthogonal to what I was saying no?
- ksec 3y agoHopefully this stayed on the front-page long enough and Andrew gets to reply from his perspective.
- laserbeam 3y agoIt's actually rare that I end up needing extra stuff attached to errors in zig. When I do, I end up thinking of a higher unit of work where I can stick some context data in case of errors. For example, the article mentions a JSON parser. In zig I'd end up writing a class with minimal state and a few methods. I'd call parser.parse(...), and if that goes bad I just fill a parser.errdata field before returning an error. This is similar to the old errno.h way of doing it in C, but it's not global. That's the pattern I use, if I need detailed error information, I make sure to provide a pointer where they can get written. And it turns out I haven't needed that too often...
- throwawaymaths 3y agoYou should consider using options error payloads. Saw an article on zig.news Benefit is if you don't want the payload, the code to generate the payload gets compiled out.
- skybrian 3y agoThis seems to be it: https://zig.news/ityonemo/sneaky-error-payloads-1aka https://zig.news/ityonemo/sneaky-error-payloads-1aka It looks like it would be useful, but relies on a naming convention? Hopefully everyone picks the same field names.
- throwawaymaths 3y agoOr, read the docs on the function you're calling?
- skybrian 3y agoI was thinking more about what happens if you’re combining already-written code that uses different naming conventions, or even different types. Suppose you get an error in one format and want to pass it on in a different format? Writing a converter probably isn’t so hard, but it’s tedious, like having multiple kinds of strings. But it’s probably too soon to worry about that when Zig doesn’t have a package manager yet.
- deleted 3y ago[deleted]
- pjmlp 3y agoEveryone complains about checked exceptions, blames them on Java, while they actually came up on CLU, Mesa, Modula-3 and C++. And at the end of the day, it turns out forced error checking is an idea everyone keeps reinventing, because it is actually useful to know what errors can be "thrown".
- Rusky 3y agoThe hard part of checked exceptions is when they start interacting with higher order code- in Java this often happens when an interface method wants to throw. Each implementation may want to throw its own set of exceptions, but the code that just works with the interface doesn't care about any of them, and existing languages don't really give you a way to express this. What you really want here is polymorphism in the exception specification, and a way for consumers to allow these new exceptions types to "tunnel" through out to the caller that knows about them. And for that to work while still providing any guarantees that exceptions are handled, it turns out you need something like a borrow checker, to prevent the interface object from escaping that outer scope! Here's a 2016 paper on the subject (with Barbara Liskov as a coauthor): https://www.cs.cornell.edu/~yizhou/papers/exceptions-pldi2016.pdf https://www.cs.cornell.edu/~yizhou/papers/exceptions-pldi201....
- pjmlp 3y agoJust like mixing errors in Rust requires third party packages and macros to sort out all boilerplate code?
- Rusky 3y agoNot really what I'm talking about here- that issue comes up when you want to combine multiple concrete error types in a single monomorphic call site, rather than when dealing with polymorphism. Rust (and other languages that use something like `Result` for errors) handles the polymorphic case a bit better than Java, because you can already use the language's usual polymorphism support for the error type. But it's still not quite as smooth as first-class polymorphic checked exceptions would be, since the interface (or trait) and its callers have to do a bit more manual plumbing in some cases. For example, a simple `impl Fn() -> T` can be instantiated with `T = Result<X, E>`, but only if the caller is just going to return the `T` directly- otherwise the error won't be propagated immediately. A slightly more annoying situation is when you have some `I: Iterator` that can fail- often you fall back to `I: Iterator<Item = Result<X, E>>`, which is not quite right, and expect the consumer to stop calling `next` if it gets an `Err`. With polymorphic checked exceptions, you could use `I: Iterator<Item = X>`, with an additional annotation that `next` may fail with an `E`. Error-oblivious combinators like `map` or `fold` would continue to work directly with `X` values, but automatically propagate `E`s to the eventual caller that knows the concrete type of `I`. (And again, crates like anyhow/thiserror don't really address this problem- they're solving a different issue entirely.)
- larusso 3y agoI tend to check the error handling capabilities of a language to see how expressive and easy it is to return errors. I personally prefer result types over optionales (e.g. the mentioned added context instead of a nothing). It seems that zigs shoots in the middle here. I like the implicit errors shown but also feel that the lack of context isn’t great. What is missing for me is chaining and a simple message. That being said I‘m also not super happy how rust manages errors. At the moment I write my tools and crates with „thiserror“ and „anyerror“ which also illustrates how errors most of the times are uses. I say most since it really depends. In libraries I tend to introduce an error type per bigger module. And wrap low level errors so it results in a chain of errors (similar how Java does it with their exceptions) This gives me the most context and a kind of breadcrumb. I hate when you execute something and a naked IO error ala „Permission Error“ is returned. But this all means a lot of extra code that starts to hide the simple implementation under the hood. For simple apps where any error means „Sorry I‘m out of luck“ I use anyerror to not write custom FromError implementations. Again this all illustrates a bit that I‘m not super happy in rust. But I actually never encountered an error system which can keep it simple and fast. The Zig examples are nearly there though! Just the missing error context …
- masklinn 3y ago> Unlike Rust's unwrap which will panic on error, I've opted to convert Error into an generic error Weird dig. Panic-ing is the entire purpose of Result::unwrap, if you want to convert between types you can... just do that.
- loeg 3y agoI did not read it as a dig. Just a (very!) notable difference.
- p0nce 3y agoWhat's the downsides? Anyone have used Zig and could report on the good and bad?
- AndyKelley 3y agoCreating a Result as in this blog post is an anti-pattern, for the reasons pointed out in the article. The best pattern that is available today is to continue to use an error union for the return type, and accept a mutable "diagnostics" parameter where it stores extra useful information about failures.
- deleted 3y ago[deleted]