Idioms for error handling with contextual information?

I’m coming from the Rust ecosystem, where error handling is often in the form of a tagged union whose variants contain arbitrary payloads e.g.

enum Error {
    Foo { x: u32, y: u32 },
    Bar { msg: String }
}

As far as I can tell, in Zig errors are just values. This leads me to my question: what are the idioms/patterns for printing and handling errors with contextual information?

I suppose you could always return your own FooResult type that’s a Result-like tagged union, but then you lose the ability to automatically propagate the error variant with try.

For a concrete example, suppose you’re parsing input from a text file and want to return the column and line at which a parse error occurs.

Add a parameter to the function signature for outputting error context.
This is how I understand this setting: when the existence of a structure is bound to the error itself, there may be some memory problems in the construction of the structure itself. For example, if a new heap memory object needs to be constructed inside an error structure, then an error in the construction process is equivalent to a new error nested inside an error, which is confusing. If the creation of the error context is handed over to the programmer and the language is decoupled from the error code, then the construction error of the error context is grammatically thrown as an independent error, rather than an error in another error construction process, avoiding nested errors.
In addition, some UAF problems may occur inside the construction process of this error structure. For rust, the language itself avoids this problem. But rust seems to have no measures for the generation of nested errors. It basically relies on the programmer’s self-consciousness in constructing the error structure to avoid nested errors.

2 Likes

Previously:

6 Likes

Ah, thank you! All of my previous searches used the word “context” and I never thought to search for “diagnostics”.

1 Like

also just prefer to handle errors where you have the context to do so, rather than propagating them up

2 Likes

In my experience, 90% of all possible errors can only be handled reasonably by giving an information upwards that there was an error and clean-up resources along the way.
For example, if you want to read text from a file, but the file doesn’t exist or you don’t have the permission to read or whatever, there is usually nothing the program can do to handle this, except informing the user and stop the processing, at least for this file.
In languages with exception handling, as a rule of thumb, it’s best to just not catch each and every exception everywhere.
Instead, there should be only a few exception handlers at a high level which inform the user and eg in case of DB applications, rollback the transaction.
Zig has errors, not exceptions, but this is just a pure technical aspect and doesn’t change the concept.
Back to the missing file example, the user needs to know where in the fs the program was looking for the file and what was the filename.
It is very helpful if the program can add this information to an error structure.
In its simplest form, this could be a string-to-string map.
Regarding added complexity due to allocating, one could use a small arena allocator (say, 4kb) which is reserved only for error info (which should be global and cleaned up in certain places ). Adding error information should use this allocator and is allowed to fail silently (it’s just adding more info to the error).

The tricky part is where that location actually is. I suspect you already know that, but for anyone reading this in the future who wants more detail you can read the comment above mine.

Another example is when I’m developing a CLI, where we want consistent styling for errors regardless of where they’re generated in the application. Say an error occurs in a library I’m using. Even if the library has context for handling the error, it shouldn’t decide that part of “handling” the error is printing a message to the consumer since I’m the one that gets to make decisions about how that should look.

My original question is actually for a CLI I’m working on. I view it as having three layers and I was wondering how people would pass “diagnostics” from the bottom layer to the presentation layer:

  • Presentation
  • My business logic and implementation details
  • Third-party code

It’s just another service that a library can provide.

Details are very much dependent on the particular service that a library might be providing.

For a language parser, or something else intrinsically hierarchical, it can easily mean the equivalent of a stack trace, which would lead you providing diagnostics as a list-like thing. Another pattern is a typed log that client code can traverse in reverse to glean the details it needs to present useful information to the user. For something quite flat, passing an optional allocator and simple out parameter can be a good solution.

The CLI using libraries is a good example.

One could say that the concept I suggested is a variation of the diagnostics pattern.
Let’s call it “GD” for global diagnostics, and the pattern used in the standard library for JSON “LD” for local diagnostics.

LD uses specialized diagnostics for each type of possible error, with fields like eg line: u32.
GD uses a generic structure: A string map or maybe string list.

In this regard, LD is more powerful if we actually want do use this data anything else but showing it to the user or a log file. But if showing it is all we need, then the string map is probably more easy to use, since it is more generic.

With LD, as seen in the examples, usually the diagnostics struct is on the stack and has to be explicitly specified before used.
This is in line with Zig’s philosophy, but at the same time a bit tedious.
An advantage would be here that you don’t need to think about cleaning it up, unless you want to reuse it in the same function for a second call (even then you should check in the code that in case of an error all fields are written to avoid leftovers from the previous error.

With GD, there would be only exactly one global diagnostics struct, using its own private memory.
(I’m thinking of a single threaded program only).
No need to pass it explicitly to each function.
In case of an error, a function should either

  • add to / adjust the diagnostics and return an error (not necessarily the same error) or
  • handle/report the error (using the diagnostics) and then clear the diagnostics.

And just to be sure, also clear the diagnostics on a high level before starting the actual action.

Anyway, both patterns require that not only your own code, but also the libraries support them.
Of course, if a library supports LD, you can use it also for GD (by formatting the diagnostic data fields to name/value strings and add them to the GD info).

As a side note, I’m coming from a background of languages with exceptions (eg. Python , Java) or quite restricted exceptions (PL/SQL), and while I’m fine with using errors instead of exceptions, the fact that it’s complicated to add useful info to errors is really a pain point.
With errors I mean basically what Python class EnvironmentError means: something outside of the program’s control.
Mere programming errors eg UB don’t need additional info, they are very well shown in the debug builds, I like that.

@hvbargen @zmitchell sorry I should have been more descriptive.

Libraries should propagate errors, if not all context is available to the caller, the library should provide access to it.

yes, there will most likely be places where you can handle most errors while having the context to do so, my opinion is simply to avoid creating those places artificialy by propogating context as that means more code to maintain and debug.

presenting the error to the user is a distinct part of error handling, I think its fine and common to seperate that part to do it seperately and more uniformly.

My prefered way of this kind of diagnostics is for GD to also have the information that you would get from LD. That creates further separation from the creation and presentation of diagnostics, there is also an argument for using less memory, though that is less important as diagnostics usually isnt a hot path.

I tolerated c++/c# exception handling, error handling in go.
I will tolerate zig also

What about this pattern?