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.
r := os.Open(src) or errorReceiveros.Open(src)returnsnil, fmt.Error("Some error")- call
errorReceiver(fmt.Error("Some error")) - If result of
errorReceiveris still an error then assignerrorReceiver’s error toexample’s return error and exit fromexample - If result of
errorReceiveris nil then continue execution ofexample
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 failurefunc(error) T1, T2, ...Tn- Error receiver that always results in recoveryfunc(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)
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 errorReceiverkey() handle errorReceiverBut 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.
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
tryproposal.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 originaltryproposal. 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.)