Description
After reading through the problem overview the try() proposal and every comment on the try() issue #32437 I like the idea of try(), thank you everyone for such hard work! The implementations so far have not solved what I view as the issue of error boilerplate, namely the scope of the err
var.
Problem
Go scopes errors using inline if
statements, but does not scope errors when a variable needs to be used after.
Scoped:
if err := foo(); err != nil {
if _, err := bar(); err != nil {
Not Scoped:
r, err := bar()
if err != nil {
When the error is not scoped it is has multiple problems. The ones that come to mind are:
- Shadowing
- All
err
s need to be the same type - The variable existing past when handling makes sense
Proposal
I propose the language add try
/handle
keywords similar to what was proposed by @james-lawrence.
r := try bar() handle(err error) {
return err
}
The try
keyword would return everything but the final value. The handle
block would only run if the final value was non-zero.
This proposal also solves many common complaints from the try
proposal #32437:
- Requiring a defer function
- Requiring a named returned which some developers try to avoid
- Implicit error handling
- Obscured
return
- Adding complexity to error decoration
Examples
CopyFile
The CopyFile
func found in the overview becomes
func CopyFile(src, dst string) error {
r := try os.Open(src) handle(err error) {
return err
}
defer try r.Close() handle(err error) {
//handle
}
w := try os.Create(dst) handle(err error) {
return err
}
defer try w.Close() handle(err error) {
//handle
}
try io.Copy(w, r); handle(err error) {
return err
}
try w.Close(); handle(err error) {
return err
}
}
The main difference between current error boilerplate and using block scoped error handling are:
- The
err
fromos.Open(src)
doesn't live for the whole scope of the function - The
defer
functions can handle their errors in the same manner as other functions
Hex
The hex
example in the overview becomes:
func main() {
hex := try ioutil.ReadAll(os.Stdin) handle(err error) {
log.Fatal(err)
}
data := try parseHexdump(string(hex)) handle(err error) {
log.Fatal(err)
}
os.Stdout.Write(data)
}
This is very similar to the current go except the err
returned from ioutil.ReadAll
isn't overwritten by the err
from parseHexdump
.
Inline/Curried functions
The try
proposal comments included examples of nested/curried functions that caused some worry such as this AsCommit
.
func AsCommit() error {
return try(try(try(tail()).find()).auth())
}
Wrapping a try
/handle
in ()
would mean a variable never needs to be saved if it is not wanted. This case is similar to using an anonymous func func()int { ... }()
and handling the error, but would allow use of other keywords like break
, continue
, and return
.
Current Go
func AsCommit() error {
t, err := tail()
if err != nil {
return err
}
f, err := t.find()
if err != nil {
return err
}
if _, err := f.auth(); err != nil {
return err
}
}
With block scoped error handling (please don't do this)
func AsCommit() error {
(try tail() handle(err error) {
return err
}).(try find() handle(err error) {
return err
}).(try auth() handle(err error) {
return err
})
}
Struct Init
type foo struct {
Value int
}
Current go
func styleA(s string) error {
f := foo{}
var err error
f.Value, err = strconv.Atoi(s)
if err != nil {
return errors.Wrap(err, "value could not be converted")
}
func styleB(s string) error {
n, err := strconv.Atoi(s)
if err != nil {
return errors.Wrap(err, "value could not be converted")
}
f := foo{
Value: n,
}
With block scoped error handling
func styleB(s string) error {
f := foo{}
f.Value: try strconv.Atoi(s) handle(err error) {
return errors.Wrap(err, "value could not be converted")
}
func styleA(s string) error {
f := foo{
Value: (try strconv.Atoi(s) handle(err error) {
return errors.Wrap(err, "value could not be converted")
}),
}