This is my proposal for handling errors in a new version of Go. This is part of the ongoing public discussion at [https://github.com/golang/go/wiki/Go2ErrorHandlingFeedback]
My proposal does not make code shorter, but provides easier error handling and cleanup.
I prefer to stick to the Go language conventions and create an extension to an if statement. Consider this:
if <expr>; err != nil {
...
} else {
...
}
Let Go provide a new extension like this:
if <expr>; err != nil {
... ERROR handling code
} else {
... SUCCESS handling code
} undo {
... if anything later fails, undo what we just did
} done {
... if everything later succeeds, cleanup (normally in a defer)
}
This is a normal if-statement that immediately executes either the THEN or the ELSE clause. When it is done with that, it appends the UNDO clause to an undo stack, and the DONE clause to a done stack and execution continues below.
The compiler should only allow undo and done clauses when the function returns an error as the last return value. The undo stack is executed when the function returns a non-nil error. The done stack is executed when the function returns a nil error.
The existing defer stack is also still available, which executes in both cases: after the done or undo stack was emptied.
Consider also adding a keyword "defer" to be used in the place of done and undo when they are both the same. Then it reads:
if <expr>; err != nil {
... ERROR handling code
} else {
... SUCCESS handling code
} defer {
... for both success/error case, do this
}
Which is just another way of writing:
if <expr>; err != nil {
... ERROR handling code
} else {
... SUCCESS handling code
}
defer {
... for both success/error case, do this
}
One can change the expression and check to be anything you can do with an if-statement. The THEN does not have to be the error and the ELSE does not have to be the success. The done/undo/defer blocks are only stacked after the THEN or ELSE completed. So if you return inside the THEN or ELSE clause, they do not apply. Hence, the success code should normally be after the if-statement, and not inside the else as shown above. Which is how we normally write code.
The complete code pattern therefore is:
if <expr>; err != nil {
... ERROR handling code
} undo {
... if anything later fails, undo what we just did
} done {
... if everything later succeeds, cleanup
} defer {
... when the function return, do this last, in all cases, after done/undo
}
... SUCCESS handling code
One of the examples in the discussion was:
func CopyFile(src, dst string) error {
handle err {
return fmt.Errorf("copy %s %s: %v", src, dst, err)
}
r := check os.Open(src)
defer r.Close()
w := check os.Create(dst)
handle err {
w.Close()
os.Remove(dst) // (only if a check fails)
}
check io.Copy(w, r)
check w.Close()
return nil
}
With the if-else-undo-done format it will read like this:
func CopyFile(src, dst string) error {
if r,err := os.Open(src); err != nil {
return fmt.Errorf("copy %s %s: %v", src, dst, err)
} undo {
r.Close()
} done {
r.Close()
}
if w,err := check os.Create(dst); err != nil {
return fmt.Errorf("copy %s %s: %v", src, dst, err)
} undo {
w.Close()
os.Remove(dst)
} done {
w.Close()
}
if err := io.Copy(w, r); err != nil {
return fmt.Errorf("copy %s %s: %v", src, dst, err)
}
return nil
}
That is a bit longer for sure, but the error handling and cleanup I believe is better and easier to plan.
Wherever the undo and done uses the same code for cleanup, one can use defer instead, because defer stack is executed after undo or done. So the code could be shortened with:
if r,err := os.Open(src); err != nil {
return fmt.Errorf("copy %s %s: %v", src, dst, err)
} defer {
r.Close()
}
The "check" keyword is introduced to shorten the code and make it easier to read. Following that philosophy, one can write if-else-undo-done as follows:
func CopyFile(src, dst string) error {
r := check os.Open(src)
fail return fmt.Errorf("copy %s %s: %v", src, dst, err)
defer r.Close()
w := check os.Create(dst)
fail return fmt.Errorf("copy %s %s: %v", src, dst, err)
undo {
w.Close()
os.Remove(dst)
}
done w.Close()
check io.Copy(w, r)
fail return fmt.Errorf("copy %s %s: %v", src, dst, err)
return nil
}
The above can already 90% be achieved with a simple sweeper object to manage an undo and a cleanup stack of functions. Here is a copy of the same code using such a struct called "Sweeper" with methods:
- Sweeper.WhenDone(f()) pushes function f to the done stack
- Sweeper.WhenUndo(f()) pushes function f to the undo stack
- Sweeper.Defer(f()) pushes function f to both done and undo stacks
- Sweeper.Fail() calls the undo stack in reverse order, then returns a forwatted error wraping the old error
- Sweeper.Success() calls the done stack in reverse order, then returns nil.
func CopyFile(src, dst string) error {
p := NewSweeper()
r, err := os.Open(src)
if err != nil {
return p.Fail(err, "copy %s %s", src, dst)
}
p.Defer(func() { r.Close() })
w, err := os.Create(dst)
if err != nil {
return p.Fail(err, "copy %s %s", src, dst)
}
p.WhenDone(func() { w.Close() })
p.WhenUndo(func() { w.Close(); os.Remove(dst) })
if _, err := io.Copy(w, r); err != nil {
return p.Fail(err, "copy %s %s", src, dst)
}
if err := w.Close(); err != nil {
return p.Fail(err, "copy %s %s", src, dst)
}
w = nil
return p.Success()
}
It is however not possible to get away from the very explicit error checking code if err != nil { ... } code which is required to return from the function after undoing the changes. If it was just possible to write the error check more compact, the code could read even better as:
...
w, err := os.Create(dst)
#err return p.Fail(err, "copy %s %s", src, dst)
p.Done(func() { w.Close() })
p.Undo(func() { w.Close(); os.Remove(dst) })
...
So the compiler simply needs to replace
#var <statement>
with
if var != nil { <statement> }
One can use it for any pointer check... But that raises the possibility to then also want to write:
!found <statement>
to execute something when found == nil or found == 0 or found == false, which would also be neat. So a statement starting with '#' checks if the variable (or expression?) is defined and a statement starting with '!' checks if the variable (or expression) is NOT defined, then execute the statement after it, like this:
user := findUser(id)
!user return fmt.Errorf("unknown user")
err := user.SetPassword()
#err return errors.Wrap(err, "failed to change user password")
return nil
...
I would like that... and then add another symbol like '+' for putting statements on the done|undo stacks, and we have:
user := createUser(id)
!user return fmt.Errorf("failed to create user")
+undo { user.Delete() }
+done { user.SendWelcomeEmail() }
err := user.SetPassword()
#err return errors.Wrap(err, "failed to change user password")
return nil
This is basically a form of
from other languages