The pattern here is to return something like error.Diagnostics for all errors that have been reported via diagnostics.
The only reason you'd have a different error code would be if the control flow should be different. For example error.OutOfMemory makes sense to be separate because it's a retryable error, which means it should be handled by different control flow.
Would you endorse the pattern I described in my article if the calling code could have other uses for the diagnostic info than just logging?
In my project I decided that the CLI main should be the place with a reporting mechanism. All the internal code is agnostic to how errors are handled. Many errors could be retried, even if they are currently not, and in some cases you also need extra diagnostic data in order to retry. For example if 1/4 plugins fails to load, I would need to know which one failed to ask the user interactively where to find it or whether to skip it before retrying. There's a few different errors like this, and layers between the handling code and the error source code, which is how I ended up with the union(enum) diagnostic keyed by error code pattern.
It seems that "error codes aren't a reporting mechanism", practically speaking, means that internal application code should just log its own error and return a generic error code that bubbles up unhandled out of main. You decided that this wasn't ok for allocation failures and encourage retrying those, which I find to be inspiring, but apparently most types of application errors are fine to essentially log and ignore. So that is confusing.
I love Zig, and look up to you a lot, so thanks for all your efforts over the years!
At a high level, all non-trivial programming is composition. And (see principle of encapsulation) the vast majority of errors shouldn't be recovered and just need to be propagated out.
Then, to be useful, errors need enough information to be diagnosed or investigated. It seems like this should have been a straight-forward requirement in the language design.
A "diagnostics" pattern has emerged in the community to optionally request extra information about a failure. You can pass a pointer to the diagnostic (it can be on the stack) and get the extra info back. It's just a more explicit version of what would otherwise happen in a language with error payloads.
Minor correction: stack traces are available on all build modes, but different build modes have different defaults. See: std.Options.allow_stack_tracing
Yes, it has error return traces.
As an example: https://github.com/kristoff-it/ziggy/blob/852053b09a5f8f5b79...
The part about mapping every error kind to different error code in Zig is debatable. It might be useful in some cases maybe (I don't have the confidence to fully exclude it), but at the very least in my experience I never ever needed that.
Outside of that, error codes are useful for debugging code that is running on other people's machines (i.e. in production) for and for reporting reasons.
> Here, sqlite.ErrorPayload.init saves 500 bytes of error message from sqlite
Who owns those 500 bytes and where are they being freed?
// sqlite.zig
pub const ErrorPayload = struct {
message: [256]u8,
pub fn init(db: *c.sqlite3) @This() {
var self = std.mem.zeroes(@This());
var fw = std.Io.Writer.fixed(self.message[0..]);
_ = fw.writeAll(std.mem.span(c.sqlite3_errmsg(db))) catch |err| switch (err) {
error.WriteFailed => return self, // full
};
return self;
}
};To me, this post is proof zig doesn't need "proper support". You can already have extremely ergonomic error payloads with existing language features.
Earlier version of this post had some language feature ideas, but then I realized Zig already had all the capabilities, so I just removed that section.
For example, I used to think it'd be nice for functions to be a namespace, so I didn't have `myFunc` and `MyFuncDiagnostics`. But then I realized that the Diagnostics type doesn't need its own name; you can just put the type in the function signature, and use a function like `diagnostics.OfFunction(fn)` to extract the type from the function definition, so you can just write this:
var diag: diagnostics.OfFunction(myFunc) = .{};
const res = myFunc(foo, bar, &diag) catch |err| ...;
As another example, I used to write out the `error{OutOfMemory, ...}` type explicitly in addition to the tagged union payload, but then realized you can generate an error set from the union tags at comptime.Do you want automatic inference of the error payload type? So zig creates a special tagged union error payloads type for you automatically? It seems complicated and maybe not a good idea. Do you really want your function to return an invisible 20 elements union on error? Do you want to call a someone else's function which returns an invisible 20 elements union on error? You know, maybe it's not a good idea.
There's a barrier to setting up the diagnostic pattern. When you're in the greenfield phase it's easy to search for information about error reporting, discover various different approaches and long threads of people arguing about what better and just say "ah screw it, I have more important things to do right now" and postpone the decision.
Your approach is fine, I don't love how verbose it is but it could probably be tweaked. If this is the way forward then it should be included in stdlib, documented, and promoted as the recommended way of reporting errors.
I think you meant extremely unergonomic. If you take a dev poll, 8/10 developers would not find the solution ergonomic at all.
Best way to do it is to pass a payload pointer into the function call. You can put it into an options struct with a default null pointer that noops if you prefer the ergonomics of a kwarg.
You can do validation at user interface and report actual nice errors.
And if something happens after that, you can save the stack trace into somewhere so the developer can see it. And you report unexpected error to user as that is exactly that
Depending on your language preference; Zig has issues[citation needed], but 1) it's still version 0 and 2) and this is the important part: who cares?
I get you have a pattern and shape you like a lot. But there's less value in that existing shape, than there is in being mildly uncomfortable, and expanding your world view and trying things in ways you wouldn't normally do.
If you tried it and didn't like it, cool, go back to the language you're already used to. But for everyone else. I'd encourage you to try doing things "wrong" for a while and seeing if you can't learn something from forcing yourself to do so.
Something especially true for a language that HN likes to pretend is just a toy and can't compete with [ language you already decided won ]
% rg Diagnostics zig/lib/std | wc -l
165
The zig stdlib kind of has it easy because, for example, the json module can have one schema for error diagnostics. An app that stitches together a bunch of libraries and has a few internal modules is going to want some different Diagnostics schemas for different errors, and sharing those schemas and bubbling them up, so that's just what this is.