Hacker Newsnew | past | comments | ask | show | jobs | submit | qouteall's commentslogin

In my opinion the biggest issue of Zig is that it doesn't allow attaching data to error. The error can only be passed via side channel, which is inconvenient and ENOURAGES TOOL DEVELOPERS TO NOT PASS ERROR DATA, which greatly increase debugging difficulty.

Somethings there are 100 things that possibly go wrong. With error data you can easily know which exact thing is wrong. But with error code you just know "something is wrong, don't know which exactly".

See: https://github.com/ziglang/zig/issues/2647#issuecomment-1444...

> I just spent way longer than I should have to debugging an issue of my project's build not working on Windows given that all I had to work with from the zig compiler was an error: AccessDenied and the build command that failed. When I finally gave up and switched to rewriting and then debugging things through Node the error that it returned was EBUSY and the specific path in question that Windows considered to be busy, which made the problem actually tractable ... I think the fact that even the compiler can't consistently implement this pattern points to it perhaps being too manual/tedious/unergonomic/difficult to expect the Zig ecosystem at large to do the same


Interestingly, I just read an article from matklad (who works a lot with Zig) talking about the benefits of splitting up error codes and error diagnostics, and the pattern of using a diagnostic sync to provide human-readable diagnostic information:

https://matklad.github.io/2025/11/06/error-codes-for-control...

Honestly I was quite convinced by that, because it kind of matches my own experiences that, even when using complex `Error` objects in languages with exceptions, it's still often useful to create a separate diagnostics channel to feed information back to the user. Even for application errors for servers and things, that diagnostics channel is often just logging information out when it happens, then returning an error.


The separation of error codes and diagnostics is fine, but the language needs a standard mechanism to optionally pass this error diagnostic information. Otherwise, everyone will develop their own different way with ZERO consistency and many will simply not pass error diagnostics at all.

Your and GP's two statements are not mutually exclusive. This paradigm can have significant benefits, and at the same time be too cumbersome for people to want to use consistently.

I agree that building a special diagnostic system is better than just using language's builtin error system. However that takes efforts.

Library developers tend to choose the path of least resistance, which is to not pass diagnostic information.

The most convenient diagonistic system is the good old logging. Logging is easy.

Maybe logging will be the de facto solution of passing error data in Zig ecosystem, due to psychological reasons.


I tend to follow the rest of the ecosystem when developing libraries. If i wanted to make a zig lib id look at what other major libs are doing (or not doing) and copy that.

If i found no consistency id be making a post like OP but from a different perspective.


If they want to keep to the error/diagnostic pattern I think they're gonna have to adopt some kind of standard context object that gets passed around. Passing an allocator, an IO implementation, and a diagnostic object all over your code base is going to get really fucking old.

if it helps: it's pretty typical to attach the allocator to the object directly, so you dont have to pass it everywhere. for library consumers, just build a global allocator and use that. io will just "have to be passed the annoying way" in libraries (but a consumer can also also easily globalize that).

I'm assuming you're talking about the "managed" pattern you'd see in stuff like Hashmaps and ArrayLists. To my knowledge that style is falling out of favor, and I think the managed versions will be removed from the standard library.

I haven't seen anyone use a global allocator in the way you're talking about, and if you did I feel like it goes directly against the Zig ethos. Part of the benefit of allocators being passed around is that all allocation are explicit.


If you're a library writer, then don't make a global allocator. If you aren't, then do whatever you feel like.

Having metadata with the error doesn't exclude having a separate diagnostics system. You don't have to use errors with metadata.

The "correct" way is highly context dependent with the added proviso that Zig assumes a low-level systems context.

In this context, adding data to an error may be expedient but 1) it has a non-trivial overhead on average and 2) may be inadvisable in some circumstances due to system state. I haven't written any systems in Zig yet but in low-level high-performance C++20 code bases we basically do the same thing when it comes to error handling. The conditional late binding of error context lets you choose when and where to do it when it makes sense and is likely to be safe.

A fundamental caveat of systems languages is that expediency takes a back seat to precision, performance, and determinism. That's the nature of the thing.


If the error rarely happens then passing error data shouldn't affect performance in visible way. If the error occurs in common path then it's designed wrongly.

I agree that in special states like OOM passing error data with allocation is not ok.


Error data being returned instead of just error codes doesn't require allocation at all, and never would, unless the specific unions that you're returning require as much. Zig already has tagged unions with a tag field and associated payload, that is exactly what you would return. The overhead isn't remarkably worse than the cost of modifying the value someone passed in to "Fill this in in case of errors" (which is what you have to do now in Zig).

For example , if error data contains string, if it directly point to input string it won't require allocation, but will have memory safety issue if error is passed to outer scope where input string is deallocated. Many errors copy string to make it usable in outer scope.

For quite a long time, I have been wondering why I like to code in Raku so much … in a round about way you set me thinking. Perhaps it’s because, in Raku, precision, performance and determinism take a back seat to expediency. (Sorry for the tangent).

Wow, Raku looks like a really interesting language, I'd never heard of it before!

Have you used it in any large projects?


I love it. My largest project is about 20k lines … so nothing too big. But if you need to be expedient (just quickly make a data extract/load/transform or a command line thingy) it is great fun. The LLMs seem to be pretty good too, just the usual hallucination here and there.

Had you heard of Perl 6?

Perl I used in 2008 or so but not since. Haven't come across Perl 6.

Perl 6 = Raku.

Please, stop deadnaming the Raku Programming Language :-)

I thought it was useful information for people who did not know this. Of course Wikipedia would have sufficed, too: "Raku, formerly known as Perl 6 [...]".

I hear you were coming from the angle of being useful. In a sense that's what matters most, and I love that you have that spirit.

If Wikipedia has deadnamed Raku with grace then that might be a model to follow, but in general it's far from helpful unless it's done carefully. There's a reason why the community embarked on the somewhat painful multi decade process of renaming it. To try clarify my understanding I'll summarize it here.

Because of the original name for Raku, people assumed for a long time (long after it became problematically misleading) that it shared semantics, or syntax, or compiler tech, or libraries, or something technical like that, with some other programming language(s).

This was partly because Raku did indeed take some inspiration philosophically and/or technically from some existing languages (traces of a half dozen or so are evident), and partly because Raku and its compiler tech features the ability to use Python code and compilers, and C code and compilers, and Perl code and compilers, and so on, as if they were native Raku code/compilers.

But Raku was never C, or Python, or Perl, and the old name is unfortunately a form of deadnaming -- which is to say, something that is seldom helpful, especially if some commentary like this comment is not included.

At least, that's how I experience it.

That said, regardless of my view, I love your impulse of being helpful, which is why I've written this -- and I hope it does help any readers.



Went to have a look to the (beautiful and informative) website for the raku language to refresh my memory, and looking at the examples I though "Oh god, those sigills, those criptic short keywords... it looks like a modern perl, I doubt we would be happy together", then I went to wikipedia to check and yes indeed, that's perl 6! I'll pass. :)

Thanks for the feedback on the raku.org site. We like sigils $ for one thing (scalar), @ for many things (array) and % for dictionaries (hash). The linguistic idea is that such words stand out as “nouns” in contrast to all the routine names which are “verbs”. In practice, after you get familiar, it really helps code to be written in an expressive way to better convey the intent. Sure, if sigils makes you glaze over then maybe it’s not for you.

Sigils are just one of the things that concern me (use of UTF-8 character for keywords is another one).

The problem with sigils is that they compose poorly when casting (refs, counts), and do not generalize to other types.

Plus, they seem to encourage the language designers to implement semantic that is "context aware" which would have been another billion dollars mistake if perl had become more popular.

In other words, that's unnecessary complexity bringing the attention to a poor type system. A bad idea that deserves to die, in my opinion.


> use of UTF-8 character for keywords [concerns me]

What do you mean? All the keywords in the standard language are ASCII. And even if you meant some other part of the standard language, they're (almost) all ASCII too.

(The most notable exceptions are use of superscript 0 thru 9 for powers -- eg `2¹⁶-1 == 65535` -- and the `«` and `»` characters, which are part of the Latin-1 aka "8 bit ASCII" character set, are aliases for the (7 bit) ASCII `<<` and `>>` tokens. I understand these exceptions may concern you, but can assure you they're not really a problem in practice.)

> The problem with sigils is that they compose poorly when casting

Are you thinking Raku sigils are like sigils in other languages, eg Perl or Javascript or PHP?

From my perspective one of the several _strengths_ of Raku's sigils is that they combine succinct compile time type constraints and composition.

Just type `@` (reads as "at") to denote a compile time enforced type constraint for the `Positional` interface, an abstract/generic datatype for any iterable integer indexed collection.

So, if you have an array of points, Raku will happily let you store it in a variable named, say, `points-array`, but naming it `@points` means Raku will compile time enforce binding of that name to an integer indexed collection and visually reflect that that is so for any human glancing at the code.

As for "casting", if you want to treat `@points` as a Single Item ("a single array of points") then just write `$@points` -- the `$` reads as Single Item -- `S` over `I`.

(Technically speaking that's not time consuming casting, but just stripping off an indirection, the optimal optimization of that scenario, but I am guessing this is semantically the kind of thing you meant.)

> (refs, counts)

Again, are you thinking that Raku's sigils are like other languages'? (They're not.)

> and do not generalize to other types.

Again, are you thinking that Raku's sigils are like other language's sigils? They are not.

Raku's `$` sigil is the generic Single Item interface for any datatype. (It can be parameterized with a data structure's typed structure.)

The `&` sigil is the generic Single Item interface for any function type. (It can be parameterized with a function's type signature.)

The `@` sigil covers any integer indexed collection. (It can be parameterized with the collection's fixed length for a fixed size array, or shape for a multidimensional structure. To parameterize a nested heterogeneous data structure's type signature, use `$` instead.)

The `%` sigil covers any name indexed collection. (It can be parameterized with the collection's key/value types. To parameterize a nested heterogeneous data structure's type signature, use `$` instead.)

> Plus, they seem to encourage the language designers to implement semantic that is "context aware" which would have been another billion dollars mistake if perl had become more popular.

Why are you mentioning Perl in a subthread about Raku? Are you aware the language was renamed precisely because so many people were completely misunderstanding the nature of Raku?

> In other words, that's unnecessary complexity bringing the attention to a poor type system. A bad idea that deserves to die, in my opinion.

If you're thinking of Perl's type system and applying what you know of that to Raku's, that's like thinking Python's type system is like Haskell's. They are very different.


People are working on this. std.zon is generally considered to be a good example of how to handle errors and diagnostics, though it's an area of active exploration. The plan is to eventually collect all the good patterns people have come up with and (1) publish them in a collection, and (2) update std to actually use them.

    > how to handle errors and diagnostics, though it's an area of active exploration
I am flabbergasted and exasperated by this sentiment. Zig is over 9 years old at this point. This feels this same kind of circular arguments from Golang "defenders" about generics and error handling.

Go gets a lot of flack for getting some things wrong but it was a stable and productive language within a couple of years.

If you look at the current Zig website the hello world example doesn’t compile because they changed the IO interface. Something as simple as writing to the console.

It’s easier to get things right if you have no issues breaking backward compatibility for a decade. It feels it’ll be well over 10 years before Zig is “1.0”.



When will we see Zig 1.0?

Agreed, this is probably my biggest ongoing issue with Zig. I really enjoy it overall but this is a really big sticking point.

I find it really amusing that we have a language that has built its brand around "only one obvious way to do things", "reducing the amount one must remember", and passing allocators around so that callers can control the most suitable memory allocation strategy.

And yet in this language we supposedly can't have error payloads because not every error reporting strategy is suitable for every environment due to memory constraints, so we must rely on every library implementing its own, yet slightly unique version of the diagnostic pattern that should really be codified as some sort of a language construct where the caller decides which allocator to use for error payloads (if any).

Instead we must hope that library authors are experienced and curious enough to have gone out of their way to learn this pattern because it isn't mentioned in any official documentation and doesn't have any supporting language constructs and isn't standardized in any way.

There must be an argument against this (rather obvious) observation but I'm not aware of it?


Genuine question, how would error set unioning work with payloads? error.WriterFailed (might be getting the exact name wrong) is returned from many different writers, whether writing to a statically allocated array, writing to a socket, or writing to a file. Each error would have a very different payload, so how would you disambiguate between the different payloads with a global error type? The way I see it is either you have error sets, or payloads, but not both.

I'm also wondering what payload people want. There's already an error handling trace (similar but different to a normal stack trace) that captures the how the error propagates up to the point it's being handled so shows you exactly where the initial point was.

A payload can contain any details that the producer of that error believes are important. I don't know what you mean by the error handling trace, how does that help with the toy JSON parsing example? I want the payload to indicate the position in the input string where the parser encountered an error.

I want a function like `diffFiles(path_a, path_b)` to have an error set of `error { ReadError }` with more detailed information in the payload (e.g. file path, translated error code). The alternative is: `error { FileAReadErrorNotFound, FileBReadErrorNotFound, FileAReadErrorPermissionDenied, FileBReadErrorPermissionDenied, ...}`


I would expect a generic interface to define a base error payload which would contain some common information and a pointer to the implementation-specific details (or embed them in the original payload as a tagged union).

Yeah, every single newbie programming language designer starts with a maximalist position of "exceptions are hard, just return an error code", and then end up inventing their own shitty, ad-hoc and malfeatured exception handling system.

I want off this ride.


Go seems to do alright without exceptions. It’s just a convention to return errors, which seems fine as a practice.

I know that Zig doesn't allow attaching data to error for valid reasons. If error data contains interior pointer then it can easily cause memory safety problem. Zig doesn't have a borrow cheker or ownership system to prevent that.

https://github.com/ziglang/zig/issues/2647#issuecomment-2670...


> I know that Zig doesn't allow attaching data to error for good reasons. If error data contains interior pointer then it causes memory safety problem

IMO this is not a good reason at all.


Changed to "valid reasons"

The problem of dangling pointers is not unique to error data.

If you wanted to have a parameter that gets filled in when there is an error, this exact issue will remain, it's completely unrelated to which language construct you use to capture errors and has more to do with having a good idea of how your errors are allocated, if they require allocation. I don't think the commenter in the GitHub issue thought this through at all, and probably didn't expect to have it be held up as some example of why you can't return tagged unions (because it's not an example of that, not even remotely).

There are plans in Zig to allow to include custom information into error stack trace https://github.com/ziglang/zig/issues/14446.

But that is not implemented.

In any case, when debugging annotating error with extra context often is not enough. One often needs a detailed trace of what happens before.

So what I would like to see in any programming language is ability to do a structured logging with extra context from the call stack (including asynchronous support in languages that have that) that has almost zero overhead when the log is not printed.

Various languages and runtimes have some libraries that try to do that, but the usage is awkward and the performance overhead is not trivial.


Not this is explicitly marked as Not Planned

I made a go of this using the stacktrace functionality built into C++23. The overhead and complexity it introduced made it not worth it, unfortunately. There may be a way to do this but it seems non-trivial in implementation.

closed as not planned :)

This is annoying. It’s because errors were designed to be a bitset and not have pointers. I would also prefer that they were a `union(enum)`.

We are free to do that as a return type like `Result(T)` and just forgo using `try`, but yeah, I wish this was in there.


I agree, I like e.g. https://doc.rust-lang.org/std/string/struct.FromUtf8Error.ht...

See, we were trying to make this data we had into a string, Rust says all strings are UTF-8 encoded - but, turns out the data wasn't UTF-8 after all, here's an error with the data inside it.

Or a really delicate piece of design, (nightly for now) Vec::push_within_capacity. We're hoping the growable array (Vec<T>) has enough space for this T, thus getting rid of it, but if not we don't want to grow the array, maybe we're bare metal software and can't afford to allocate on this hot path, so we get back an error with the T we were trying to push onto the array inside it, so we can do something else with that T but only when the problem happened, otherwise it's gone.


I can see pros and cons. Preventing data being attached to an error forces more clear and precise errors.

Whereas lazy devs could just attach all possible data in a giant generic error if they don’t want to think about it.


> Preventing data being attached to an error forces more clear and precise errors.

Okay maybe theorically, but in the real world I would like to have the filename on a "file not found", an address on a "connection timeout", a retry count on a "too many failures", etc.


But also in the real world I may not be interested in any error information for the library I’m using. I’d like to be able to pass a null for the error information structure and have the compile optimize away everything related to tracking and storing error information.

I’d like my parser library to be able to give me the exact file, line and column number an error occurred. But I’d also like to use the library in a “just give me an error if something failed, I don’t really care why” mode.


I don't follow, because there's a possibility that someone somewhere might create a bad overly-generic error set if they were allowed to stuff details in the payload when those should be reflected in the error "type", it's a good idea to make the vast majority of error reporting bad and overly-generic by eliminating error payloads entirely?

Id rather have data in a generic error type than no data in a specific error type.

How useful is a file not found error type without data (the filename) when the program is looking for 50 files? Not very.

How useful is a generic error type with '{filename} not found' as generic string data packed in? Quite.


This seems kinda contrived. In practice that "ERROR DATA" tends not to exist. Unexpected errors almost never originate within the code in question. In basically all cases that "ERROR DATA" is just recapitulating the result of a system call, and the OS doesn't have any data to pass.

And even if it did, interpreting the error generally doesn't every work with a microscope over attached data. You got an error from a write. What does the data contain? The file descriptor? Not great, since you really want to know the path to the file. But even then, it turns out it doesn't really matter because what really happened was the storage filled up due to a misbehaving process somewhere else.

"Error data" is one of those conceits that sounds like a good idea but in practice is mostly just busy work. Architect your systems to fail gracefully, don't fool yourself into pretending you can "handle" errors in clever ways.


I think you've skipped over all the cases where knowing the filename is actually helpful? It's true that sometimes it isn't.

Also, a line number is often helpful, which is why compilers include it. Some JSON parsers omit that, which is annoying.


> Also, a line number is often helpful

That's not error data, that's (one level of) a stack trace. And you can do that in zig, but not by putting call stack data into error return codes.

The conflation between exception handling and error flagging (something that C++ did largely as a mistake, and that has been embraced by managed runtimes like Python or Java) is actually precisely what this feature is designed to untangle. Exception support actually turns out to have very non-trivial impact on the generated code, and there's a reason why languages like Rust and Zig don't include them.


> That's not error data, that's (one level of) a stack trace.

They're not talking about the stack trace, but about the common case where the error is not helpful without additional information, for example a JSON parsing library that wants to report the position (line number) in the string where the error appears.

There's no way of doing that in Zig, the best you can do is return a "ParseError" and build you own, non-standard diagnostic facilities to report detailed information though output arguments.


Another way to look at this example is that, for the parser, this is not an error. The parser is doing its job correctly, providing an accurate interpretation of its input, and for the parser, this is qualitatively different from something that prevents it doing its job (say, running out of memory).

At the next level up, though, there might be code that expects to be able to read a JSON config file at a certain location, and if it fails, it’s reasonable to report which file it tried to read, the line number, and what the error was.

Sure, but that's a different level with different considerations. The JSON parser shouldn't care about things like ‘files’ with ‘locations’; maybe it's running on some little esp8266 device that doesn't have such things.

Error data should specify where the error occurred and what failed. So you'll know which file had a problem, and that the problem in question was a failure to write. From that you can make the inference that maybe the disk is full, etc.

Seralizing error data to text and then dumping that in a log can be pretty useful.

Simplify: tokio::select! will discard other futures when one future progress.

The discarded futures will never be run again.

Normally when a future is discarded it's dropped. When a future holding lock is dropped, lock is released, but it's passing future borrow to select so the discarded future is not dropped while holding lock.

So it leaves a future that holds a lock that will never run again.


Thanks for suggestion.


I added that into article


I added clarification

(That simplified example is just for illustrating contagious borrow issue. The *`total_score` is analogous to a complex state that exists in real applications*. Same for subsequent examples. Just summing integer can use `.sum()` or local variable. Simple integer mutable state can be workarounded using `Cell`.)


In part 2 https://www.bitflux.ai/blog/memory-is-slow-part2/ the last diagram still shows that memory is faster than disk.


Thanks for your suggestions.


Added


Thanks for reminding. Added.


updated


Guidelines | FAQ | Lists | API | Security | Legal | Apply to YC | Contact

Search: