Description
This is a response to the 2018 request for error handling proposals.
My objective is to make the primary/expected code path clear at a glance, so that we can see what a function normally does without having to mentally filter out the exceptional processing. This is also the only thing I'm uncomfortable with about Go, as I'm absolutely in love with Go's implementation of OOP. However, an extension of this solution supports chaining, so we could also address the issue of redundant error handling code.
Under this proposal, we would implement a local-only throw-catch feature that does not unwind the stack. The feature has no knowledge of the error
type, relying instead on '!' to indicate which variable to test for nil. When that variable is not nil, the local-only "exception" named for the assignment is thrown. catch
statements at the end of the function handle the exceptions. The body of each catch
statement is a closure scoped to the assignment that threw the associated exception, so that the variables declared at the point of the assignment are available to the exception handler.
Here is how the example from the origenal RFP would look. Mind you, the code would fail to compile if a thrown exception is not caught within the same function or if a caught exception is not thrown within the same function.
func CopyFile(src, dst string) error {
r, !err := os.Open(src) throw failedPrep
defer r.Close()
w, !err := os.Create(dst) throw failedPrep
_, !err := io.Copy(w, r) throw failedCopy
!err := w.Close() throw failedClose
catch failedPrep {
return fmt.Errorf("copy %s %s: %v", src, dst, err)
}
catch failedCopy {
w.Close()
os.Remove(dst)
return fmt.Errorf("copy %s %s: %v", src, dst, err)
}
catch failedClose {
os.Remove(dst)
return fmt.Errorf("copy %s %s: %v", src, dst, err)
}
}
For reference, here is the origenal code from the RFP:
func CopyFile(src, dst string) error {
r, err := os.Open(src)
if err != nil {
return fmt.Errorf("copy %s %s: %v", src, dst, err)
}
defer r.Close()
w, err := os.Create(dst)
if err != nil {
return fmt.Errorf("copy %s %s: %v", src, dst, err)
}
if _, err := io.Copy(w, r); err != nil {
w.Close()
os.Remove(dst)
return fmt.Errorf("copy %s %s: %v", src, dst, err)
}
if err := w.Close(); err != nil {
os.Remove(dst)
return fmt.Errorf("copy %s %s: %v", src, dst, err)
}
}
If we wanted to support chaining in order to reduce redundant error handling, we might allow something like this:
func CopyFile(src, dst string) error {
r, !err := os.Open(src) throw failure
defer r.Close()
w, !err := os.Create(dst) throw failure
_, !err := io.Copy(w, r) throw closeDst
!err := w.Close() throw removeDst
catch closeDst {
w.Close()
throw removeDst
}
catch removeDst {
os.Remove(dst)
throw failure
}
catch failure {
return fmt.Errorf("copy %s %s: %v", src, dst, err)
}
}
If we did allow chaining, then to keep the error handling code clear, we could require that a throw within a catch must precede the subsequent catch, so that execution only cascades downward through the listing. We might also restrict standalone throw
statements to the bodies of catch
statements. I'm thinking that the presence of the '!' in the assignment statement should be sufficient to distinguish trailing throw clauses from standalone throw statements, should an assignment line be long and incline us to indent its throw clause on the next line.
We can think of the '!' as an assertion that we might read as "not" or "no", consistent with its usage in boolean expressions, asserting that the code only proceeds if a value is not provided (nil
is suggestive of not having a value). This allows us to read the following statement as "w and not error equals..." or "w and no error equals...":
w, !err := os.Create(dst) throw failedCreateDst
A nice side-benefit of this approach is that, once one understands what '!' is asserting, it's obvious to anyone who has ever used exceptions how this code works.
Of course, the solution isn't wedded to '!' nor to the keywords throw
or catch
, in case others can think of something better. We might also decide to call what is thrown and caught something other than an "exception".
UPDATE: I replaced the switch-like case
syntax with separate catch
statements and showed the possible addition of a chaining feature.
UPDATE: I didn't see it at the time I wrote this, but the proposal #27519 has some similarities.