Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

"errorset" is confusing #1668

Closed
binary132 opened this issue Oct 20, 2018 · 15 comments
Closed

"errorset" is confusing #1668

binary132 opened this issue Oct 20, 2018 · 15 comments
Milestone

Comments

@binary132
Copy link

binary132 commented Oct 20, 2018

Creating this after discussion in #760 (comment).

I'm reading the documentation for Errors (https://ziglang.org/documentation/master/#Errors) and I think this feature is too convoluted. Maybe there's a way for most users to opt in to minimal functionality and error API surface, with a more expressive error API available if needed.

Please feel free to clarify my understanding, since I see Zig mostly from the outside, and not as a user. However, you hopefully will eventually have many such users, and it is important to make it accessible, usable, and readable. This is especially important to the API designs of third-party libraries which evolve out of the syntax and semantics of the language.

I'll give an example of a convention in Go that makes things clean and simple in error APIs. I know the most about Go, but I also know that exception handling can be really painful, and so can error chains in Rust, so I understand that type systems for error values are important to get right. I don't have that much familiarity with how other languages do this, or if there's a cleverer and better option out there.

Error APIs can make languages anywhere from very simple to very complicated to use. Consider a highly expressive template-metaprogrammed exception typesystem in C++ as the worst example. Please take care to be thoughtful about the design of error handling, since it could be a shining gold star on Zig's overall design.


Anyway, in Go there's a convention, which I really appreciate, to prioritize the design of the error API of a function type as follows. Each example shows why the complexity of API design and usage increases with each approach.

  1. No errors in the return type
    v := foo()
  2. Some error in the return type, but it's binary to the user, and orthogonal to the return type. ("There was an error, so I will pass it back" vs. "there was not an error.")
    if v, err := foo(); err != nil { ... } else { ... }
  3. Some error in the return type, which is checkable for a particular property, e.g.: https://golang.org/pkg/os/#IsNotExist but may be implementation-specific. The user shouldn't interact with the API of this error value except indirectly.
    if v, err := foo(); os.IsNotExist(err) { ... } else if err != nil { ... } else { ... }
  4. Some error in the return type, which may conform to a particular interface:
if v, err := foo(); err != nil {
    if eT, ok := err.(Resetter); ok {
        // err has some special behavior.
        eT.Reset(state)
    }
    return err
} else { ... }
  1. Some error in the return type, which may conform to more than one interface:
v, err := foo()
switch eT := err.(type) {
case nil:
    // fall through to main logic
case Warner:
    eT.Warn(state)
    // implied switch break, go to main logic
case Resetter:
    eT.Reset(state)
    return err
default:
    return err
}
  1. An error with a specific struct type, that the user may want to extract from the error interface and interact with: https://golang.org/pkg/os/exec/#ExitError
if v, err := foo(); err.(os.ExitError) {
    // parse something from stderr.
    // notice that the user needs to understand the struct type for os.ExitError.
    if regexp.MustCompile("asdf").Match(err.(os.ExitError).Stderr) {
        // uh oh, asdf is bad
        panic("ID10T: asdf!")
    }
}

return err
@PavelVozenilek
Copy link

I'd once proposed simple error mechanism (#122), which was completely "local", i.e. it required no outside constructs like error sets, didn't pollute function signature and didn't require to predefine anything.

I also proposed ability to disable language features which some people may find too complex: #1027. Current error system was one of suggested.

@emekoi
Copy link
Contributor

emekoi commented Oct 20, 2018

zig error handling scheme is pretty simple imo. an error set is the set of all the error a function can return. take for example this:

const std = @import("std");

cont File = struct.{
    const OpenError = error.{
        AccessDenied,
        OutOfMemory,
        FileNotFound,
    };

   const AllocationError = error.{
       OutOfMemory,
   };
   fn open(name: []const u8) OpenError!File {
       // open a file...
   }

   fn write(self: *File, data: []const u8) !void {
       // write to a file
   }

   fn copy(self: *const File) !AllocationError {
       const copy = // copying can return null... orelse return error.OutOfMemory
       
   }
}

the function File.open can only return an error in the error set OpenError. the write method can return any error at all because it doesn't have an explicit error set in it's declaration so it uses the global error set which contains every error there is. it doesn't even have to be defined anywhere. the method copy can only return the OutOfMemory error contained within the AllocationError error set. but note that OutOfMemory is also in the OpenError error set. this means the AllocationError is a subset of OpenError and AllocationError errors can be implicitly cast to OpenError errors.

what i dislike about zig error handling is that when you use the global error set and you mistype the name of an error, for example OutOffMemory instead of OutOfMemory, it's not an error, you just created a new error. it's kind of like how misspelling a variable name is lua creates a new variable, not an error. what i think would be better is to have the global error set contain all explicitly declared errors. or have a way to declare the error set to be that of all the errors of the functions inside a function's body.

update: see here

@PavelVozenilek
Copy link

@emekoi: there are two places which need to care/know about the error:

  1. Place where it is created/thrown.
  2. Place(s) where it is handled/caught.

Everything else is not needed. Declaration of error set is busywork with no real value; function signature with errors just makes them harder to read/change, it is also redundant (if compiler enforces handling of every error). Subsets/supersets and implicit casting takes the thing to C++ complexity level .

Having one global error set for all errors per project is not practical, like making one huge C enum listing all possible error constants in the program. It becomes unmaintainable very quickly.

@tgschultz
Copy link
Contributor

tgschultz commented Oct 20, 2018

the write method can return any error at all because it doesn't have an explicit error set in it's declaration so it uses the global error set which contains every error there is.

This is incorrect. When no error set is specified -- as in fn foo() !void --, the error set is inferred by the compiler, which is to say that the compiler is aware of all the errors that could possibly be thrown by the function or by calls made within the function and automatically creates an error set comprised of them. To specifically use the global error set representing any error, you use the form fn foo() error!void.

The value of ErrorSets is that you can have the compiler error for you if you accidentally mis-type an error name (as in OutOffMemory mentioned by emekoi), and more clearly communicate the possible errors a function can produce. It can also be used to limit the errors an interface is allowed to produce, as is the case with OutOfMemory and the std.mem.Allocator interface.

@Hejsil
Copy link
Contributor

Hejsil commented Oct 20, 2018

Well, any good C libraries specify an enum of all errors and documents a subset that each function can return. In Zig, we just have a feature for this, so we don't need to document these things, and get it wrong.

@PavelVozenilek
Copy link

@Hejsil: this feature is redundant. If the compiler makes sure that every possible error is handled, then there's no need to repeat that information within the code.

Imagine doing this:

  • You need to return an error. You write return error.MARTIANS-HAD-LANDED; and nothing else. No need for pre-declare anything, create hierarchies, whatever.

  • If you call that function, you either already know the need to handle error.MARTIANS-HAD-LANDED, or the compiler will tell you. Nobody else needs to care about this, there would be no confusing constructs somewhere in the code, function signature will stay clean and change-resistant. Low level details would not leak to other places, it would be resistant to typos. Anybody reading the code calling that function would clearly see all errors (being handled), there would be no loss of information or ambiguity.

Now compare this with the current mechanism which uses several highly abstract concepts, including "implicit cast". So much complexity just to return and handle error.

@Hejsil
Copy link
Contributor

Hejsil commented Oct 20, 2018

How would the compiler track indirect calls if the error set is not part of the function signature?
How would you ensure, that platform dependent code have the same interface?

For some code, these things don't matter, and you can use error.SomeName, try, catch and inferred error sets. This is the simple subset of the error handling mechanism. When you need to solve the problems above, that is when error sets are used.

@PavelVozenilek
Copy link

@Hejsil: Indirectly called functions either must not use errors (e.g by turning them into regular return value), or the compiler could do the whole program analysis. The first solution would be more desirable for me. Errors are/should be handy shortcuts, so that one does not need to create yet another useless helper type, when the dealing with that error is so trivial. If things get complicated, as with indirect calls, then errors are no longer desirable feature, but obstacle and should not be used.

Platform dependent errors would handled by platform dependent code. I think (but could be mistaken) that this is implementable and intuitive to use.

@Hejsil
Copy link
Contributor

Hejsil commented Oct 22, 2018

Any vtable/dynamic dispatch feature will not be able to use the error handling features of Zig then and Allocator cannot return error.OutOfMemory then.

Errors are/should be handy shortcuts, so that one does not need to create yet another useless helper type, when the dealing with that error is so trivial.

And they are. You don't have to declare any useless helpers for any trivial code that calls functions directly. Removing error sets will just make it so you have to write "useless" helpers for any indirect call, where you'd really wanna use the great error handling mechanism of Zig. If handeling errors is nonetrivial, then people will just ignore the errors. C is a good example of this.

Platform dependent errors would handled by platform dependent code. I think (but could be mistaken) that this is implementable and intuitive to use.

And what about platform independent abstractions? Because of Zig's lazy analysis, we cannot infer all possible errors for all platforms. The best option for these abstractions is to define a superset for all platforms.

@PavelVozenilek
Copy link

@Hejsil: Maybe vtable mechanism could employ use some error annotation. This would be special case, ordinary functions should not need anything.

It could be, that I am completely behind the times here - after 0.1.0 Zig added so many strange things, that I am often lost what still works and how. (This did happen to me before, with language Nim, after watching its development for few years. When I eventually dared to write something in it, I was overwhelmed. Nim is powerful, full of features, but incoherent and short on good docs.)

@andrewrk andrewrk added this to the 1.0.0 milestone Oct 23, 2018
@networkimprov
Copy link

@andrewrk since this issue relates to Go error handling...

The Go community is in the midst of a heated debate about how, and whether, to enhance its minimalist (many would say simplistic) error handling. That discussion has surfaced a variety of ideas about error annotation and wrapping, and function- or package-scope error handlers. (For the past year, I've been the Go community's most active author re new features for error handling outside Google.)

I think the Zig team might benefit from reviewing some of this material. Zig has some features that Go lacks, e.g. try and catch, but its Errors feature set seems a little thin by comparison to the rest of the spec. If you're interested, I'd be happy to open an issue and provide a collection of relevant links and commentary.

Thanks for your consideration, and kudos for undertaking an ambitious and necessary project!

@GoNZooo
Copy link

GoNZooo commented Jul 28, 2019

I think the Zig team might benefit from reviewing some of this material. Zig has some features that Go lacks, e.g. try and catch, but its Errors feature set seems a little thin by comparison to the rest of the spec.

Since this is an issue about error handling you might consider just posting the links relevant to the issue to add to the discussion.

With regards to Go's error handling, last I checked (which admittedly is a while ago, a Google search doesn't reveal any new info on that front) it doesn't have the capability to do exhaustiveness checks on anything, not even "Did you check at all?". There's value in not being overly dismissive, but I have a hard time seeing how an error handling system with that basis is one to take inspiration from.

@networkimprov
Copy link

networkimprov commented Jul 28, 2019

@GoNZooo I prefer not to debate Go error handling with folks who don't use the language. FYI, you missed the "Error Values" feature, landing in 1.13, and discussions around the check/handle and try() proposals.

Re links, I don't wish to hijack this thread.
EDIT: And the collection of links and commentary isn't a short post.

@GoNZooo
Copy link

GoNZooo commented Jul 28, 2019

@GoNZooo I prefer not to debate Go error handling with folks who don't use the language. FYI, you missed the "Error Values" feature, landing in 1.13, and discussions around the check/handle and try() proposals.

Re links, I don't wish to hijack this thread.

It seems reasonable to me that things germane to this discussion about error handling are brought up if they can contribute to the knowledge space so that people can make informed choices, yours truly included.

@andrewrk
Copy link
Member

andrewrk commented Apr 15, 2020

All the planned ways to change how error sets work in the language are noted in other open issues.

@andrewrk andrewrk modified the milestones: 1.0.0, 0.7.0 Apr 15, 2020
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

8 participants