How to figure out the IO error type?

I wrote pretty-printing code based on the Zig pretty printer (below).

The compiler is complaining that my Error type is not including the the some IO error set(s). How do I, generally, figure out the error set in cases like this?

Thanks, Joel
const PPS = pprint.PrettyPrintingStream(ArrayList(u8).Writer);

fn dumpExpr(alloc: Allocator, expr: IR.Expr) Error!void {
    const w = std.io.getStdOut().writer();
    var bw = std.io.bufferedWriter(w);
    const stdout = bw.writer();
    var buffer = std.ArrayList(u8).init(alloc);
    buffer = std.ArrayList(u8).init(alloc);
    var stream = PPS{
        .indent_delta = 2,
        .w = buffer.writer(),
    };
    var pp: Printer.PrettyPrinter = .{
        .gpa = buffer.allocator,
        .pps = &stream,
        .tree = undefined,
    };
    try Printer.printExpr(&pp, expr);
    w.writeAll("\n") catch {};
    _ = try stdout.write(buffer.items);
    try bw.flush();
}

The code…

const PPS = pprint.PrettyPrintingStream(ArrayList(u8).Writer);

fn dumpExpr(alloc: Allocator, expr: IR.Expr) Error!void {
    const w = std.io.getStdOut().writer();
    var bw = std.io.bufferedWriter(w);
    const stdout = bw.writer();
    var buffer = std.ArrayList(u8).init(alloc);
    buffer = std.ArrayList(u8).init(alloc);
    var stream = PPS{
        .indent_delta = 2,
        .w = buffer.writer(),
    };
    var pp: Printer.PrettyPrinter = .{
        .gpa = buffer.allocator,
        .pps = &stream,
        .tree = undefined,
    };
    try Printer.printExpr(&pp, expr);
    try w.writeAll("\n");
    _ = try stdout.write(buffer.items);
    try bw.flush();
}
1 Like

in this case its std.posix.WriteError

2 Likes

Either by following the source code/documentation to see if there is an explicitly defined error set, or alternatively by using error{} (the empty error set) to then let the compiler complain about errors that are missing from the declared error set.

1 Like

You have to add the errors one by one if using an empty error set, no?

Will the compiler tell me the error set I’m missing?

Yes.

All the missing errors get printed line by line:

...
test.zig:9:40: note: 'error.Unexpected' not a member of destination error set
test.zig:9:40: note: 'error.AccessDenied' not a member of destination error set
test.zig:9:40: note: 'error.DiskQuota' not a member of destination error set
test.zig:9:40: note: 'error.SymLinkLoop' not a member of destination error set
...

I suppose automatic generation of explicit error sets could be fixed in ZLS at some point, since right now you can literally just copy the error set the compiler gives you.

2 Likes

Where I have landed, zig has impractical error handling for practical use-cases.

Yes, the grammar supports errors, and yes, errors are a thing. However, the methods by which errors are handled and what the errors are, at times, completely opaque and unknown. Is this a skill issue? If the answer is yes, should it be?

Spending time to handle errors often results in a experience where more time is spent on the paradigm of error handling vs actually handling the intended error. This, to me, makes the paradigm impractical.

Very quickly, a common pattern emerges.- since we don’t know what we can’t learn, we have no choice but to propagate errors to a parent, ultimately landing in main. Since we don’t know what the errors might be, we just pass on whatever errors we got. This to me isn’t error handling; why mandate error propagation? is it that much more straightforward?

tl/dr, try everything, return !<type> aka anyerror, and let main sort it out!

!<type> and anyerror!<type> are not the same thing. First one is inferred error set, second is literally any error possible.

5 Likes

!<type> and anyerror!<type> are the same thing

fixed :slight_smile: I realize semantically they are different but functionally and philosophically…what is the difference? They are both a set of unknown errors…semantically different yet conceptually identical. One includes the other, which changes the code written…the pattern taking precedence over the product.

No they aren’t the same, the moment you use anyerror!<type> you can’t pragmatically use switch(errorset) anymore to handle errors exhaustively, because now that would require you to write a case for every error type within the entire program.

With anyerror you would have to deal with countless errors that don’t even happen in that piece of the code, which would cause you to want to use non-exhaustive switch, which then wouldn’t cause compile errors for unhandled new error cases.

With the inferred error set you can exhaustively switch on error sets that have accumulated to still have a reasonable size, thus you have a tool to deal with errors explicitly and when your program changes you will get a compile error if the switch doesn’t handle a new error that was added to that inferred set.

5 Likes

you would have to deal with countless errors that don’t even happen in that piece of the code

The result is the same, tragically.

And that is the unfortunate reality is that this is true for most code. The author doesn’t know what the possible errors are, doesn’t know how how to handle the unknown set.

Thus there is no functional difference - both sets of errors are the same philosophical set: do not care because the code cannot know.

On a good day, a developer will switch the one error they do know, and need to handle gracefully,.and else for the rest…and now,what is different? What has been achieved?

The “don’t know so I don’t care”" case is the base case for error handling. And shouldn’t there be a less demanding solution?

Flexibie sugar is what make things sweet…

When you switch on an inferred error set, handling one error and passing on the rest in else, something has been achieved, the else error set has been shrunk by that specific error that was handled. Making it easier for other code to handle the remaining errors in some other switch exhaustively.

And those remaining errors are still a lot less than all the possible errors in the entire program.

Too much syntactic sugar in the language gives it diabetes and makes it dependent on insulin injections.

I think things are getting too philosophical and metaphorical for me, I don’t understand what your point is because it is described so abstractly.

Can we get back to talking about precise semantics and the results of those language constructs on handling error cases?

If you as the programmer use else in error handling switches to be lazy, then the blame for that is on the programmer, I don’t see how it is the languages fault, if you chose a lazy way to handle things and then get bitten by it later.

That is like not using any optional type annotations in dynamic languages and then complaining when your function gets called with unexpected values.

At some point programmers have to take responsibility for the code they write, instead of pushing the blame on to the language. If you don’t use the available tools, then the language can’t do anything about it.

I guess it could force you to handle everything like go does, through trite repetition, or use exceptions to then get the complaint “but exceptions happen that I am not handling and can’t predict”…

At least with Zig you can see the errors that can happen along a specific code path and without having to use if err != nil {return err} instead of try.

4 Likes

If you as the programmer use else in error handling switches to be lazy,

At some point programmers have to take responsibility for the code they write, instead of pushing the blame on to the language. If you don’t use the available tools, then the language can’t do anything about it.

Thank you for your thoughts, it is helping me. I apologize for the abstraction.

I do think zig can do something about it; i don’t think it’s lazy, it’s knowledge.

My contention is although zig is intended to have three error handling cases (identified error set, inferred ! error set, all errors aka anyerror), and this is GOOD, for many use cases, it is confusing.

So we end up, often, with:

  switch (...) {
      ...
      else =>  ...
  }

Adding a new concept to the convo;

I have been mulling over the difference between grammar and language, and perhaps there are two zig languages that both implement the zig grammar: the zig language of people, and the zig language for the compiler / language server.

The zig language server is key to clarity. An IDE can render the explicit while the person types the implicit; we see this today with inferred types, param names,e tc, and it is wonderful…

What I think should happen is … zig should require no error handling at all.

There could be a fourth case, 100% inferred.

  • There is no try.
  • There is no !.
  • There is no catch.
  • There is no switch on error set, no else.

In the OP case, they could ignore error handling altogether. I think the compiler is smart enough to do what is expected for the programmer unless there is an explicit case.

But for that fourth case to really be useful, the zig language server would have to fill in the blanks. I should probably start a new post so i don’t fill up the poor OPs inbox with this clutter :).

Only if you capture the error set in the else:

foo() catch |err| switch (err) {
    error.Something => {},
    else => |e| return e, // narrowed to not include error.Something
}

Here’s an example of something that’s basically only possible because of this aspect of Zig:

In general, I think it would be worth your while to look at how often errors are switched on in the Zig standard library. It’s not a superfluous feature at all.

2 Likes

horrible idea,
zig has syntax specificaly to make error handling more ergonomic and to make the easiest way to deal with an error a valid strategy, you would be removing those ergonomics, granted in most cases it wouldnt be that different but still.

worst case we end up with the same kind of mess as c with error handling, i dont think this would happen though.

best case we use a Result type like rust, upsides error payloads, downside you have to create error super sets which gets really frustrating. zig solves the issue of error super sets, its one of my favourite features

yea no, thats even worse, thats re introduces the main issue with exceptions, that being you have no idea what is going on regarding errors, it also required the lsp to do a lot more work to inform you and it wouldnt be perfect at that either.

both of your suggestions, while it would make the language simpler, would make using the language more complex and goes against zig’s philosophy.

1 Like

It would turn it into a different language, something more like ocaml (which is one of the languages I would consider using if there was no Zig).

1 Like

it also required the lsp to do a lot more work to inform you and it wouldnt be perfect at that either.

Why wouldn’t we want the lsp to do more work? I would argue the lsp would likely to a better job than people…for the inferred case. Note, the language grammar is unchanged, nothing removed, nothing invisible…but what a person types…yes…fewer errors? More correct code? Fewer short cuts?

I think the genius of zig is clarity of grammar; a language implements grammar, why not have two languages, the one a person keys in, the one a language server renders? That is the beauty of the inferred case, it is no less accurate than the explicit case.

lsp’s are already complicated pieces of software making it do more work makes it harder to maintain, harder to improve, harder to update when zig realease a new version.

what you are proposing is removing clarity

why make things more complicated unecissarily

i really dont understand why you want to make zig worse just because a computer can do some of the work.

just because it can, doesnt mean it should. many people are conveying they dont think it should do this work, why do you think they are wrong

1 Like