Description
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 errorReceiver
os.Open(src)
returnsnil, fmt.Error("Some error")
- call
errorReceiver(fmt.Error("Some error"))
- If result of
errorReceiver
is still an error then assignerrorReceiver
's error toexample
's return error and exit fromexample
- If result of
errorReceiver
is 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.