The error design draft is a good step in simplifying both the reading and writing of variety of code. I was even surprised that it efficiently allows readable handling of errors within http handlers:
func ListHandler(r Repo) {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
handler err {
http.Error(w, http.StatusText(500), 500)
}
data := check r.List()
fmt.Fprintf(w, data)
})
}
While this simple example doesn't show it, there are a lot of handlers in the wild that have a lot of repetitive error handling code, which not only makes it a pain to write, but also to read. And the handlers have similar LIFO semantics as defers, so its not all foreign.
In a way, even if the design draft is implemented as is, it will be a good step forward.
But it could be improved a bit.
The draft calls this piece of code the "handler" and treats it as a block of code. The reason for this is that somehow the return
within it has to be explained. Its like an inline function, but without all the special magic afterwards.
But why can't it be a function, and not some block of code? What if the handler accepts two types of functions:
- a function that returns the same type it accepts
- a function that accepts a pointer and doesn't return anything
Then within, the chain semantics would be well defined. A func(t T) T
would stop the chain, whereas a func(t *T)
would not.
The CopyFile example would look something like this:
func CopyFile(src, dst string) error {
handle func(err error) error {
return fmt.Errorf("copy %s %s: %v", src, dst, err)
}
r := check os.Open(src)
defer r.Close()
w := check os.Create(dst)
handle func(err *error) {
w.Close()
os.Remove(dst) // (only if a check fails)
}
check io.Copy(w, r)
check w.Close()
return nil
}
This looks a bit more familiar. Now, lets adopt the draft's syntax as a proper syntax for defining anonymous functions [with type inference]:
t {
return z
}
(t, u) {
return z
}
// Or without inferred types
(t T) Z {
return z
}
(t T, u U) Z {
return z
}
Now, the proposed draft code will fit within a more familiar grammar:
func CopyFile(src, dst string) error {
// err is inferred as (err error) since the function returns 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)
// err is inferred as (err *error) since the function doesn't return
handle err {
w.Close()
os.Remove(dst) // (only if a check fails)
}
check io.Copy(w, r)
check w.Close()
return nil
}
Both of these functions have correctly inferred arguments, since handle
only accepts two types of functions.
Finally, a very minor concern:
The draft design elevates error
to an even higher pedestal than every other interface. Rather implement check
and handle
to accept any type, and invoke handle
only when the type is non-zero. Not that people will be using these constructs for anything other than errors, it just seems over-the-top to add such a special casing restriction.
I don't view it as a counter proposal, in fact I'm mostly for it. Wouldn't a counter proposal be something completely different?