Explicit error checking and the broad exception catching problem
As I was writing yesterday's entry on a subtle over-broad try
in Python, it occurred to me that one
advantage of a language with explicit error checking, such as Go, is that a broad exception catching problem
mostly can't happen, especially accidentally. Because you check
errors explicitly after every operation, it's very hard to aggregate
error checks together in the way that a Python try
block can fall
into.
As an example, here's more or less idiomatic Go code for the same basic operation:
for _, u := range userlist { fi, err := os.Stat(u.hdir) if err != nil || !(fi.IsDir() && fi.Mode().Perm() == 0) { fmt.Println(u.name) } }
(Note that I haven't actually tried to run this code so it may have a Go error. It does compile, which in a statically typed language is at least a decent sign.)
This does the stat()
of the home directory and then prints the user
name if either there was an error or the homedir is not a mode 000
directory, corresponding to what happened in the two branches of the
Python try
block. When we check for an error, we're explicitly checking
the result of the os.Stat()
call and it alone.
Wait, I just pulled a fast one. Unlike the Python version, this
code's printing of the username is not checking for errors. Sure, the
fmt.Println()
is not accidentally being caught up in the error check
intended for the os.Stat()
, but we've exchanged this for not checking
the error at all, anywhere.
(And this is sufficiently idiomatic Go that the usual tools like
go vet
and golint
won't complain about it at all. People ignore
the possibility of errors from fmt.Print*
functions all the time;
presumably complaining about them would create too much noise for a
useful checker.)
This silent ignoring of errors is not intrinsic to explicit error
checking in general. What enables it here is that Go, like C, allows you
to quietly ignore all return values from a function if you want instead
of forcing you to explicitly assign them to dummy variables. The real
return values of fmt.Println()
are:
n, err := fmt.Println(u.name)
But in my original Go code there is nothing poking us in the nose
about the existence of the err
return value. Unless we think about
it and remember that fmt.Println()
can fail, it's easy to overlook
that we're completely ignoring an error here.
(We can't do the same with os.Stat()
because the purpose of calling
it is one of the return values, which means that we have to at least
explicitly ignore the err
return instead of just not remembering
that it's there.)
(This is related to how exceptions force you to deal with errors, of course.)
PS: I think that Go made the right pragmatic call when it allowed totally ignoring return values here. It's not completely perfect but it's better than the real alternatives, especially since there are plenty of situations where there's nothing you can do about an error anyways.
Sidebar: how you can aggregate errors in an explicit check language
Languages with explicit error checks still allow you to aggregate errors together if you want to, but now you have to do it explicitly. The most common pattern is to have a function that returns an error indicator and performs multiple different operations, each of which can fail. Eg:
func oneuser(u user) error { var err error fi, err := os.Stat(u.hdir) if err != nil { return err } if !(fi.IsDir() && fi.Mode().Perm() == 0) { _, err = fmt.Println(u.name) } return err }
If we then write code that assumes that a non-nil
result from
oneuser()
means that the os.Stat()
has failed, we've done exactly
the same error aggregation that we did in Python (and with more or less
the same potential consequences).
|
|