5 ms·
The error thing to me hints more at how people use errors in the wild than anything specifically about Zig per se. The example that comes to mind is propagating
by lhorie 5y ago
The error thing to me hints more at how people use errors in the wild than anything specifically about Zig per se. The example that comes to mind is propagating parsing errors from a recursive descent parser. Typically one just wants to unwind the stack with some user friendly string error message indicating what exactly went wrong, but philosophically speaking, a parse error isn't really exceptional, it's an expected code path for invalid input.
The way zig seems to think about errors is that early returns via `try` shouldn't be leveraged as a control flow mechanism to bail and bubble up values across the call stack. I think rather than saying why the error handling is done the way it is, it'd be more valuable if there were documentation on how one is supposed to go about coding to be able to use this bail-to-bubble-up-values pattern. Currently, the workaround is indeed somewhat verbose as it requires mimicking the `try` mechanism (and/or using some sort of global error store or whatever).
- anonymoushn 5y agoI didn't really understand. What is the problem and what is the workaround? Can you give an example?
- defen 5y agoThe problem (which is a deliberate design decision) is that Zig's error unions, which have special built-in support at the syntax level, are just tagged unions with an integer payload. In other words, when you return an error (either implicitly via `try` or directly with `return error.MyError`), that error payload is just an integer indicating which error was returned. So to use OP's parser example, there's not a built-in way to attach some sort of diagnostic info to the error itself, the way you would with, for example, python exceptions. Using the built-in syntax, you can't do the equivalent of `raise ParserException(token, line_number)` and then unpack that error later in your code to display a nice error message. The workaround would be to store that info in some sort of global struct (which has all the usual drawbacks of global variables); add a context/diagnostics parameter to your function, or manually manage your own error union in userspace, which means you lose the nice `try` syntax.
- lhorie 5y agoThe problem is that in other languages, it's common to do this sort of idiom throw new Error("Expected foo, found bar at line " + line) // ...then elsewhere catch (e) { log("In file " + file + ":\n" + e.message); } In zig, you can't attach arbitrary strings to errors because errors are effectively enums (in the sense of that constant names map exclusively to ints). You can only do this `try callMe() catch return error.Something` (The `return error.Something` part is zig's "equivalent" to `throw error`, but notice how I can't tack on the error message string to the error) `try` only shortcircuits into the `catch return` expression for errors, but for cases like parsers you want to shortcircuit and bubble up error messages simultaneously. You can kinda do weird workarounds by having a pointer to an error field in your input struct and mutating that prior to returning an error value, and/or doing weird nullable-to-sentinel-value-promotion shenanigans with idioms like `callMeMaybe() orelse return someSentinel("Expected foo")` to simulate the early-return-from-expression semantics from `try callMe() catch return error.Something`.
- Hoppetosse 5y agoIf you want to handle the error and also bubble it up, maybe even changing the what error bubbles up, you can just return an error from within the catch block after doing whatever handling you want. Either the captured error or something else. Specifically for recursive descent parsers, I pass around a reference to an regular struct that acts as an ErrorInfo object, and use that to pass diagnostic information up the stack. Control flow is still managed by the error returns, any additional info goes in the parameter. There is (was?) a proposal to add that capability to the language but Im not sure it had much traction.
- lhorie 5y agoYeah I think a lot of people have been settling for that pattern, but it's admittedly not ideal, in the same way that it's generally preferable to work with discrete values in a functional/pure fashion than shuffling values in and out of an out param list everywhere.
- samatman 5y ago> a parse error isn't really exceptional And a parse error should (I will argue) not be what Zig thinks of as an error. There are languages where try/catch became in effect the poor man's coroutine, and it's doubly confusing with parser "errors" because this is not something we need to handle as equivalent to an actual error in our program. A parser error is normal control flow. I would suggest that in Zig one might use suspend/resume to pass a parser error to something which prints an error, possibly returning with a 'fix' which lets parsing continue and maybe find more errors. https://ziglearn.org/chapter-5/ https://ziglearn.org/chapter-5/
- lhorie 5y agoI agree that conflating errors with stack unwinding is an unfortunate reality of how exceptions work in most languages. I'm not sure if I agree that suspend/resume are good alternatives. Conceptually, they seem most similar to C's setjmp, which I think we all can agree is generally not the first tool of choice for pretty much anything that isn't a complete hackjob. The other workarounds at least look somewhat transparent about their costs. Even continuation passing style might be something I could grok. But what is the cost of a suspension frame? I legitimately have no idea. Another issue is that suspend/resume are conceptually highly procedural and I personally find it difficult to reason about in recursive contexts (e.g. can I/should I pass suspension frames around? What idiomatic idioms from other languages does doing such a thing map to? Do I need to manually allocate heap for recursion like I need to for async recursion? Is it reasonable to expect a e.g. golang person to be ok with that sort of stuff?)
- samatman 5y agoIt's also hard for me to tell if Zig's async mechanism is a good choice here, tbh. Stackful asymmetric coroutines are a good choice, these are stackless but an error is one stack frame so it's unclear that's a problem in practice. I shrug a little too much when it comes to Zig, although documentation and tutorials improve every time I come back to take a look. My main point is that a parse error isn't a program error, even if it's implemented with exceptions unwinding the stack for one reason or another (and this should be avoided if possible).
- throwawayzig 5y agoI think more importantly, the error return mechanism in zig creates a trace, and for parser early returns you don't want a trace, so a 'richer' struct union mechanism makes sense.