[HN Gopher] Error Handling in Zig
       ___________________________________________________________________
        
       Error Handling in Zig
        
       Author : nalgeon
       Score  : 142 points
       Date   : 2023-08-06 09:54 UTC (13 hours ago)
        
 (HTM) web link (www.aolium.com)
 (TXT) w3m dump (www.aolium.com)
        
       | [deleted]
        
       | ksec wrote:
       | Hopefully this stayed on the front-page long enough and Andrew
       | gets to reply from his perspective.
        
       | AndyKelley wrote:
       | Creating 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]
        
       | pjmlp wrote:
       | Everyone 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".
        
         | masklinn wrote:
         | > 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".
         | 
         | The problem of java's checked exceptions is not that being
         | explicit is bad, it's that java's implementation is terrible.
        
           | LelouBil wrote:
           | Yeah, exceptions and try/catch blocks have way worse
           | ergonomics that Result<Err,Ok>.
        
             | pjmlp wrote:
             | Then why does Rust need third party crates for sane
             | ergonomics?
        
               | veber-alex wrote:
               | Because there isn't a one size fits all solution to error
               | handling?
               | 
               | Because before anyhow/thiserror became popular there were
               | 5 other popular crates for handling errors and if Rust
               | added one of those to std we would be stuck with a subpar
               | solution forever?
               | 
               | Because cargo is so easy to use that your preferred error
               | handling solution is one `cargo add` command away?
               | 
               | Because for simple cases the tools the standard library
               | offers are good enough?
        
           | pjmlp wrote:
           | More like, most people rather complain than learn how to use
           | them properly.
        
         | peheje wrote:
         | For my work, I usually catch general exceptions and handle
         | their aftermath, like putting the error in an error queue to be
         | manually fixed and replayed or showing an error page. While
         | there are domains like power plants or car software where every
         | error must be meticulously handled, my approach suits my
         | domain.
         | 
         | When I see code making me catch numerous unique exceptions, it
         | often hints at an API design issue. A more refined design might
         | encapsulate such information in a response, maybe through a
         | discriminated union or an enum with a message. If I can, I'd
         | refactor it to match this ideal. If not, I'd use adapters to
         | convert diverse exceptions into standardized errors.
         | 
         | Exceptions should be exceptional: situations like memory
         | shortages, database disconnects, or accessing a disposed
         | resource. For these unforeseen events, control flow is
         | typically the same as they're unexpected and beyond my control.
        
         | Rusky wrote:
         | The 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-
         | pldi201....
        
           | pjmlp wrote:
           | Just like mixing errors in Rust requires third party packages
           | and macros to sort out all boilerplate code?
        
             | Rusky wrote:
             | Not 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.)
        
         | hgs3 wrote:
         | Perhaps a controversial opinion, but I think exceptions should
         | only be thrown when something exceptional happens. Languages,
         | like Java and Python, overuse them e.g. failing to open a file
         | is not exceptional, but running out of memory is.
        
           | pjmlp wrote:
           | Running out of memory is an Error, not Exception, in Java's
           | case, and the throw semantics are different.
        
         | grumpyprole wrote:
         | I also thought checked exceptions in Java were fantastic. They
         | are a form of statically checked effects. I would like to see
         | more of this sort of thing not less.
        
         | wredue wrote:
         | Inflexibility is an actual problem.
         | 
         | In rust, before anyhow and thiserror, you'd see some pretty
         | shitty hacks for the inflexible error system, such as just
         | making all errors just a string.
         | 
         | It is clear that having all the errors in a list is actually
         | good now, but that doesn't stop programmers from hating writing
         | boilerplate.
         | 
         | Again, before anyhow, if you did error _properly_ , your
         | errors.rs had huge swathes of From implementations. Error.rs
         | boilerplate often outstripped your actual code.
         | 
         | The complaints that it's hard to change interfaces is bad, as
         | it's difficult to change interface methods regardless.
        
           | pjmlp wrote:
           | The fact that Rust requires third party crates for proper
           | error handling ergonomics, proves the point it is only
           | partially designed.
        
             | wredue wrote:
             | It's not partially designed so much as the type system
             | demands it for rust.
             | 
             | Very unfortunately for rust, making errors not just
             | maddening boilerplate forces you to trade compile time for
             | reasonable errors (although, honestly, anyhow "feels" hacks
             | to me). Compile time is already a place rust struggles as
             | it is.
             | 
             | I wouldn't bank on rust style languages having any
             | semblance of good ergonomics for errors. But at the same
             | time "you can just ignore it" is really not great either.
             | 
             | Zig errors are actually pretty nice to work with, but as is
             | pointed out, they struggle with producing really good
             | messages, or giving more information back.although, I will
             | say that I nearly never need to send more information back,
             | and there are patterns to help with that.
             | 
             | Still, if there was a language concept for it, that would
             | be nicer. It's actually not an easy problem for zig and the
             | core foundations of the language. Just like it's not an
             | easy problem for rust and its core foundations.
             | 
             | Errors are just really shitty and, as yet, I don't think
             | there exists good ergonomics. I personally haven't seen a
             | language that does them well.
        
       | p0nce wrote:
       | What's the downsides? Anyone have used Zig and could report on
       | the good and bad?
        
       | nightowl_games wrote:
       | Good 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 wrote:
         | IIRC, 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 wrote:
         | > 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
        
       | larusso wrote:
       | I 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 wrote:
       | > 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 wrote:
         | I did not read it as a dig. Just a (very!) notable difference.
        
       | valenterry wrote:
       | I 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.
        
         | aldanor wrote:
         | "goodResult | barError | bazError" method just don't scale;
         | eventually, especially if using third-party libraries, you may
         | end up with dozens of not hundreds of error types this way.
         | 
         | The way Rust does it with its try operator (?), you have to
         | tell it if it's barError (in which case it's up to you to tell
         | it how bazError is convertible to it), or perhaps bazError, or
         | something else altogether (e.g. sum type like you suggested,
         | but conversion still needs to be supplied). There's also some
         | libraries that provide error types for the lazy that are
         | convertible from everything, so you can use try operator
         | without much thinking, but for libraries you almost never use
         | them.
        
           | valenterry wrote:
           | I use exactly that way of error handling and I find it scales
           | very very well and makes refactoring a breeze.
           | 
           | Nothing stops you from defining certain "borders" in your
           | application where you wrap errors into something meaningful.
           | E.g. if I write a file-IO library, I will return a custom
           | "CannotOpenFile" error in my public methods which will
           | encapsulate the underlying error(s) instead of returning a
           | list of potential low-level errors had caused it.
           | 
           | The difference is that with uniontypes it is _me_ who decides
           | the level of abstraction and granularity, whereas without it,
           | I 'm forced into manually handling errors types pretty much
           | _everywhere_.
           | 
           | Rust is an example where you are always forced to either
           | align error types or where you lose information about what
           | kind of errors it could be by using a more abstract error
           | type. (which is the last thing you mentioned)
        
         | jmull wrote:
         | > there is no need for such a "special" language feature as
         | described here
         | 
         | I think the special language feature _is the point_ , not an
         | unfortunate side effect of something lacking in the language
         | design.
         | 
         | The purpose of errors is to pass an indication of control-flow
         | from the called function to the caller. That's why the language
         | has "catch", "try", "errdefer", etc. This is about the language
         | providing good flow control tools for the error path.
        
           | valenterry wrote:
           | Sure. My point is: if a language has certain other
           | fundamental language features, then there is no or less need
           | for those "specific" error features like errdefer.
        
         | conaclos wrote:
         | You 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
        
           | MrJohz wrote:
           | Wouldn'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 wrote:
             | No, 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.
        
         | tsimionescu wrote:
         | Sum types are still a poor error handling strategy compared to
         | exceptions, which actually implement the most common error
         | handling pattern automatically for you (add context and bubble
         | up). Having the language treat errors as regular values fails
         | to separate these fundamentally different paths through the
         | code. It also makes the programmer re-implement the same code
         | pattern over and over again.
         | 
         | This cost may be necessary in manual memory management
         | languages, where the vast majority of functions have to do some
         | cleanup. There, having multiple exit points from a function
         | through exceptions (or through early return statements) makes
         | it harder to make sure all resources are properly cleaned up
         | (especially when the cleanup of one resource can itself throw
         | an error).
         | 
         | But in managed memory languages, there's just no reason to
         | manually re-implement this pattern, either through Go style
         | regular values or through sum types. And note that the Result
         | monad is not a good substitute for exceptions, as it doesn't
         | actually add the necessary context of what the code was doing
         | when it hit an error case.
        
         | kosherhurricane wrote:
         | > If a programming language supports union types and type
         | inference then there is no need for such a "special" language
         | feature
         | 
         | Laughs in Go.
         | 
         | > While Zig's errorhandling is not bad and miles above Go's
         | 
         | Laughs again in Go. Go has no "foundations" regarding errors,
         | it's just a convention. It has no union types. It doesn't have
         | weird corner cases. It's just a returned value you can handle.
         | Or not.
         | 
         | Of all the error handling paradigms I've seen, Go's requires
         | the least amount of "specialized thinking" (try/catch or
         | whatnot)--it's just becomes another variable.
        
           | grumpyprole wrote:
           | Go's lack of sum types mean that there is no static check for
           | whether the error has actually been handled or not. Go's
           | designers went to all the trouble of having a static type
           | system, but then failed to properly reap the benefits. Sum
           | types are the mathematical dual of product types. It makes
           | sense for any modern language to include them.
        
             | Tozen wrote:
             | A good point. Newer languages influenced by Go, like
             | Vlang[1], have sum types partially for such reasons.
             | 
             | [1]:
             | https://github.com/vlang/v/blob/master/doc/docs.md#sum-
             | types
        
             | kosherhurricane wrote:
             | > Go's lack of sum types mean that there is no static check
             | for whether the error has actually been handled or not.
             | 
             | I dunno, my IntelliJ calls out unhandled errors. I imagine
             | go-vet does as well.
        
               | grumpyprole wrote:
               | A simple syntactic check will only ever work as a
               | heuristic. Heuristics don't work for all cases and can be
               | noisy. The point is, no modern language should need such
               | hacks. This problem was _completely solved_ in the 70s
               | with sum types.
        
             | dthul wrote:
             | Ever since I learned of sum types, they have ruined my
             | enjoyment of programming languages which don't have them. I
             | sorely miss them in C++ for example (and std::variant is
             | not a worthy alternative). I don't understand why any new
             | language wouldn't have them.
        
               | koolba wrote:
               | Pedantic typechecking is like learning to spot improper
               | kerning, you think it's a good thing but you spend your
               | entire life cringing at the world around you.
        
               | valenterry wrote:
               | Just wait until you learn union types, type classes, type
               | providers, and so on. It's even worse afterwards. :-)
        
             | lpapez wrote:
             | In theory the lack of sum types sounds like a drawback for
             | Go error handling, in practice it does not matter at all
             | IMO.
             | 
             | So far I have never worked a Go project without a strict
             | linter enabled on the pipeline checking that you handled
             | the case when err != nil. I don't care if it is the
             | compiler or the linter doing it, the end result in practice
             | is that there actually is no chance of forgetting to check
             | the error, and works just as well as a stronger type system
             | while also making the code stupidly obvious to read.
        
               | grumpyprole wrote:
               | > no chance of forgetting to check the error, and works
               | just as well as a stronger type system
               | 
               | A linter-based syntactic check is no substitute for a
               | proper type system. A type system gives a machine checked
               | proof. A heuristic catches some but not all failures to
               | handle errors, it will also give false positives.
        
         | martinhath wrote:
         | How would errdefer work in the general union setting?
         | 
         | Having errors as a first class construct in the language allows
         | things like errdefer to be very simple and easy to use. It
         | looks needlessly specialised at first, but I think it's
         | actually a really good design.
        
           | masklinn wrote:
           | > How would errdefer work in the general union setting?
           | 
           | Well it could not be, and some would argue that would be
           | better.
           | 
           | But you could also have a blessed type, or a more general
           | concept of "abortion" which any type could be linked to.
           | 
           | Or you could have a different statement for failure returns
           | that way you can distinguish a success and a failure without
           | necessarily imposing a specific representation of those
           | failures.
        
           | valenterry wrote:
           | That's a very good question! Most advanced languages have
           | some way of defining the concept of a "computation within a
           | context". For example, all languages that support a notion of
           | Monads do have that kind of support. Examples would be
           | Haskell, Scala, F#, ...
           | 
           | In those languages there are (or would be) generally two ways
           | of achieving the same thing as errdefer:
           | 
           | 1.) having a common interface/tag for errors
           | 
           | In that case, if you have a return type "success | error1 |
           | error2" then error1 and error2 must implement a common global
           | interface ("error") so that you can "chain" computations that
           | have a return type of the shape of "success | error".
           | "success | error1 | error2" would follow that shape because
           | the type "error1 | error2" is then a subtype of "error".
           | 
           | 2.) Having some kind of result type.
           | 
           | This would be similar to how it works in rust or in the
           | example in the article here. So you would have a sumtype like
           | "Result = either success A or failure B" and the errors that
           | are stored in the result-failure (B) would then be
           | uniontypes.
           | 
           | The chaining would then just be a function implemented on the
           | result-type. This is personally what I use in Scala for my
           | error handling.
           | 
           | Just to make it clear, this "chaining" is not specific for
           | error-handling but a very general concept.
        
         | throwawaymaths wrote:
         | You can stuff your error payload in an in-out parameter in an
         | options tuple. This way you don't lose errdefer and if you
         | don't want it the code for it doesn't exist (deleted at
         | comptime)
        
         | duped wrote:
         | > The return type will simply have the form "goodresult |
         | error" and the compiler will infer it based on the function-
         | body alone.
         | 
         | I'm 0% convinced that inferred return types are a feature that
         | a production language should have, ever. I've used it before
         | and it's infuriating on top of being bad code.
         | 
         | If a function is annotated f: T -> U where U is not an error,
         | the contract with the caller is that f does not error, and
         | cannot be changed to produce an error, and the compiler upholds
         | this contract for all callers and definitions or redefinitions
         | of f. It also means that another compilation unit can refer to
         | f without having to type check its body, which sure, is a leaky
         | abstraction, but empowers parallel compilation.
         | 
         | Changing the signature is a breaking change and as such
         | _should_ have a higher amount of friction. Changing the error
         | type doubly so.
         | 
         | And on top of all that, errors should not be non discriminated
         | union types. The example is Result<int, int> - you need the
         | error to have a discriminant to distinguish between a success
         | and an error code.
        
           | naasking wrote:
           | > If a function is annotated f: T -> U where U is not an
           | error, the contract with the caller is that f does not error,
           | and cannot be changed to produce an error, and the compiler
           | upholds this contract for all callers and definitions or
           | redefinitions of f
           | 
           | The OP is not suggesting that. They're suggesting that U can
           | be an extensible union type that can be inferred from the
           | code. Of course, if you declare it to not be such a union,
           | then you guarantee no errors.
           | 
           | Just think of it like checked exceptions from Java, except
           | the exception signature is inferred from the function's code.
           | If you explicitly declare "does not throw", then the compiler
           | produces a type error that you have not handled all
           | exceptions.
        
           | golergka wrote:
           | > Changing the signature is a breaking change and as such
           | should have a higher amount of friction.
           | 
           | Why?
           | 
           | I'm all for very rigorous static type checking. But if we can
           | make refactorings and changes frictionless -- let's. Having
           | the ability to move things around and reorder them to work
           | better results in more iterations and better code.
           | 
           | As long as each iteration is very rigorously type-checked, of
           | course.
        
             | adamwk wrote:
             | The issue is that the API can change unintentionally,
             | breaking clients because of an implementation change.
        
               | naasking wrote:
               | Implementation changes can already break clients, except
               | without checked errors/exceptions, they do so silently.
        
               | adamwk wrote:
               | Yes, certain language designs make it easier to break
               | APIs for clients, just like the one mentioned above. I
               | was pointing out that this is bad and we should try to
               | prevent that from happening
        
               | naasking wrote:
               | No, the one mentioned above makes it easier to not break
               | clients. A language that exports an API without checked
               | exceptions can change the implementation and break
               | clients silently in production. A language with checked
               | exceptions will not break clients silently, ever. That's
               | strictly superior from a robustness perspective.
               | 
               | If, as a library author, you want to define a strict
               | contract with clients that you never break, then you
               | explicitly specify the exception signature and you don't
               | let the exceptions be inferred. Then if the
               | implementation changes in a way that introduces a new
               | exception that doesn't match the client contract, then
               | the API author gets the error at compile-time.
        
               | golergka wrote:
               | It seems that you're talking about a code that is exposed
               | to some third-party clients where you need to maintain
               | compatibility. This is, of course, a valid concern, and
               | you're absolutely right that this layer, exposed to third
               | party clients absolutely does need increased level of
               | friction.
               | 
               | However, most applications don't have any such exposure,
               | all of their code is consumed internally. And even when
               | they do, their external API surface which is actually
               | exposed as some REST or ABI is minuscule in comparison to
               | all the internal APIs -- method calls and types that are
               | never exposed anywhere.
               | 
               | I think that this problem, which is certainly a valid
               | one, should be solved with methods which would not
               | negatively affect developer experience of working with
               | 99% of all the completely private APIs that don't need to
               | be backwards compatible.
        
           | valenterry wrote:
           | As others have been said, I think there is a misunderstanding
           | here.
           | 
           | First, methods can (and often should) be annotated with their
           | return types. And if a method is annotated as "T -> U" then
           | it cannot return an error and trying to do so would fail the
           | compilation - so I totally agree with you on that.
           | 
           | But inside of (non public) application or library code, there
           | are a lot of small methods calling each other before there is
           | some higher-up public method that will be called not by me
           | but someone else.
           | 
           | And for those kind of methods type inference is great when it
           | comes to refactoring. Even if I make a mistake and make such
           | a method return an error even though it shouldn't, this
           | mistake will be caught a bit higher up where the return type
           | is annotated. In my experience this has almost no drawbacks
           | and drastically simplifies refactorings.
        
             | duped wrote:
             | I don't think there's a misunderstanding, I stand by what I
             | said.
             | 
             | Inferred return types don't make refactoring easier. In my
             | experience they make it much more difficult, because you
             | have to look at function implementation to understand what
             | it does. Particularly for error handling, you always want
             | to see what errors a function may return _even in library
             | code_.
             | 
             | And all that said, there's no distinction to me between
             | internals and externally facing code when it comes to
             | quality and standards. If you have a function, even if it
             | is called once, it should always have its return type
             | annotated. It is more correct than leaving it to be
             | inferred by the compiler, because you are explicitly
             | annotating the intended behavior of the abstraction. This
             | makes it easier to write correct code and for verifying it
             | by a reviewer.
        
         | nyberg wrote:
         | The 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 wrote:
           | I think what you are saying is pretty orthogonal to what I
           | was saying no?
        
             | rootlocus wrote:
             | I'm not really experienced with programming language design
             | or with compilers, but it seems to me the design of a
             | systems programming language has to compromise on the side
             | of performance. If the implementation of the design
             | requires additional space or cpu time, it may not be a good
             | fit for the language. As such, it's not orthogonal.
        
         | amluto wrote:
         | The union approach fails when, say, barError is the type of a
         | success return somewhere. Sum types (as in Rust, for example)
         | avoid this problem.
        
           | valenterry wrote:
           | Well, yes and no. What you say also applies if two errors
           | have the same type but you want to differentiate where it
           | comes from. In all these cases you can wrap the result or
           | error into something, which essentially means tagging it (but
           | without actually needing sumtypes as a language concept.
           | 
           | Sumtypes sure are useful, but I believe that except for
           | GADTs, you can emulate sumtypes with uniontypes, but not vice
           | versa.
        
         | matklad wrote:
         | Note that Zig supports general sum types and pattern matching.
         | It however deliberately chooses not to use those features for
         | idiomatic error handling.
        
       | laserbeam wrote:
       | It'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 wrote:
         | You 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 wrote:
           | This seems to be it: 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 wrote:
             | Or, read the docs on the function you're calling?
        
               | skybrian wrote:
               | I 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.
        
               | dns_snek wrote:
               | > But it's probably too soon to worry about that when Zig
               | doesn't have a package manager yet.
               | 
               | Just fyi in case you haven't seen the news, It launched
               | with 0.11 release a couple of days ago.
        
               | throwawaymaths wrote:
               | Agreed. Probably tedious. But straightforward, unlike
               | say, multiple inheritance, and, likely to be rare.
        
             | throwawaymaths wrote:
             | The article was updated: https://zig.news/ityonemo/error-
             | payloads-updated-367m
        
       ___________________________________________________________________
       (page generated 2023-08-06 23:01 UTC)