go: proposal: Go 2: error handling with error receiver function

Hello everyone,

What if a function that receives only an error could be used as an error receiver for a failed function call?

func errorReceiver(err error) error {
    return fmt.Errorf("some error: %v", err) // do something
}

OK this may be completely crazy also maybe someone already suggested something similar but hear me out…

Overview

// Higher order function that returns an error receiver
func errorHandler(src string) (func(error) error) {
    return func(err error) error {
        return fmt.Errorf("Failed to open file %s: %v", err)
    }
}

func example(src string) error {
    errorReceiver := errorHandler(src)
    r := os.Open(src) or errorReceiver

    // do something with r

    return nil
}

Assume that os.Open fails with an error, since the signature of errorReceiver takes an error as argument the errorReceiver could be called with the os.Open error. Lets see what that would do to the order of execution.

  1. r := os.Open(src) or errorReceiver
  2. os.Open(src) returns nil, fmt.Error("Some error")
  3. call errorReceiver(fmt.Error("Some error"))
  4. If result of errorReceiver is still an error then assign errorReceiver’s error to example’s return error and exit from example
  5. If result of errorReceiver is nil then continue execution of example

With only one new language concept we are able to reusable handle errors e.g.

func errorHandler(src, dst string) (func(error) error) {
    return func(err error) error {
        return fmt.Errorf("copy %s %s: %v", src, dst, err)
    }
}

func CopyFile(src, dst string) (err error) {
    errorReceiver := errorHandler(src, dst)

    r := os.Open(src) or errorReceiver
    defer r.Close()

    w := os.Create(dst) or errorReceiver
    defer func() {
        w.Close()
        if err != nil {
            os.Remove(dst) // only if a “try” fails
        }
    }()

    io.Copy(w, r) or errorReceiver
    w.Close() or errorReceiver

    return nil
}

If in addition to this go was also able to infer the return type of higher order functions then you could end up with pretty clean syntax for error handlers. The above error handler rewritten would look something like.

// The return type of func errorHandler(src, dst string) is implied to be func(error) error
func errorHandler(src, dst string) => func(err error) error {
    return fmt.Errorf("copy %s %s: %v", src, dst, err)
}

Although even without it, this would be a significant improvement.

Handling errors and recovering from them

What if you actually want to handle the error not just simply enrich it? A more sophisticated implementation of error receiver could be implemented. So far all examples of error receiver returned an error, lets go back to my initial example.

func errorHandler(src string) (func(error) error) {
    return func(err error) error {
        return fmt.Errorf("Failed to open file %s: %v", err)
    }
}

What if error receiver here return just the *File, which is the first return value of os.Open, e.g.

func errorReceiver(err error) *File {
    return &File{} // return some default file instead
}

func example(src string) error {
    r := os.Open(src) or errorReceiver

    // do something with r

    return nil
}

Since errorReceiver is no longer returning an error but the expected value of os.Open then the result of errorReceiver could just be assigned to r and the execution of the example could continue, this effectively handles the error. Note that if os.Open returned more than one non error value e.g. func Open() (T1, T2, ...Tn, error) then error receiver would have to have the following signature func(error) (T1, T2, ...Tn)

More concretely error receiver would have the following valid signatures:

  • func(error) error - Error receiver that always results in a failure
  • func(error) T1, T2, ...Tn - Error receiver that always results in recovery
  • func(error) T1, T2, ...Tn, error - Error receiver that may recover or fail

Final example to show a case where an error receiver itself might recover or fail

func errorHandler(user string) (func(error) error) {
    return func(err error) error {
        return fmt.Errorf("User %s profile could not be found: %v", user, err)
    }
}

func downloadProfileFromAnotherSource(user string) (func(error) (Profile, error)) {
    return func(err error) (Profile, error) {
        profile := serviceB.Find(user) or errorHandler(user)
        return profile, nil
    }
}

func DownloadProfileA(user string) (Profile, error) {
    profile := serviceA.Find(user) or downloadProfileFromAnotherSource(user)
    return profile, nil
}

Conclusion

I think this is a rather neat approach since the only new thing added here is, admittedly, a slightly bizarre behaviour of error receiver when it returns, although the official try(...) error handling proposal was effectively doing the same thing, everything else here is just basic functions. Or am I just having brainfarts? Anyway, I hope someone will find this interesting.

Thanks.

About this issue

  • Original URL
  • State: closed
  • Created 5 years ago
  • Reactions: 29
  • Comments: 19 (4 by maintainers)

Most upvoted comments

I don’t think symbol should be the focus here as we can easily use something else, in fact I don’t think I like 1 character symbol at all, since this affects the flow of control it should probably be a keyword:

  • key() or errorReceiver
  • key() handle errorReceiver

But more importantly are there any fundamental flaws with this approach that I’m not seeing, would this solve majority of issues that people have with the current error handling and most importantly is this go like. These are the questions I don’t have answers to.

I was not aware try proposal had this variant, also if we ever have anything to help with redundant error handling I do not believe it will be massively different from try, at least for as long as we are trying to solve the same problem.

It wasn’t. It appeared in a few comments, but it was never part of the proposal itself. Which is a problem, since one of the biggest criticisms was that you couldn’t easily add context to the error (you’d have to use named returns and defers, which added quite a bit of verbosity).

Your proposal, to me, seems to cover 2 of the biggest criticisms of the original try proposal.

With this one, you can easily add context to an error. You can also have helper functions in the stdlib that can make it even easier (e.g.: fmt.ErrorHandler(fmt string, args ...interface{}) func(error) error).

Unlike try, you are not proposing a function, therefore there will be no confusion as to whether this is a function or some sort of control flow. I think this was the biggest mistake of the original try proposal. Whether this proposal goes with a symbol, or (my personal preference) a keyword (or), there can be no confusion as to what the code does. With the proper keyword, a reader might not even need to have knowledge of the language to have an educated guess as to what the construct might do.

Timed out in state WaitingForInfo. Closing.

(I am just a bot, though. Please speak up if this is a mistake or you have the requested information.)