If you have written any Go code you have probably encountered the built-in
error
type. Go code uses error
values to
indicate an abnormal state. For example, the os.Open
function
returns a non-nil error
value when it fails to open a file.
The following code uses os.Open
to open a file. If an error
occurs it calls log.Fatal
to print the error message and stop.
You can get a lot done in Go knowing just this about the error
type, but in this article we'll take a closer look at error
and
discuss some good practices for error handling in Go.
The error type
The error
type is an interface type. An error
variable represents any value that can describe itself as a string. Here is the
interface's declaration:
type error interface { Error() string }
The error
type, as with all built in types, is
predeclared in the
universe block.
The most commonly-used error
implementation is the
errors package's unexported errorString
type.
You can construct one of these values with the errors.New
function. It takes a string that it converts to an errors.errorString
and returns as an error
value.
Here's how you might use errors.New
:
A caller passing a negative argument to Sqrt
receives a non-nil
error
value (whose concrete representation is an
errors.errorString
value). The caller can access the error string
("math: square root of...") by calling the error
's
Error
method, or by just printing it:
The fmt package formats an error
value
by calling its Error() string
method.
It is the error implementation's responsibility to summarize the context.
The error returned by os.Open
formats as "open /etc/passwd:
permission denied," not just "permission denied." The error returned by our
Sqrt
is missing information about the invalid argument.
To add that information, a useful function is the fmt
package's
Errorf
. It formats a string according to Printf
's
rules and returns it as an error
created by
errors.New
.
In many cases fmt.Errorf
is good enough, but since
error
is an interface, you can use arbitrary data structures as
error values, to allow callers to inspect the details of the error.
For instance, our hypothetical callers might want to recover the invalid
argument passed to Sqrt
. We can enable that by defining a new
error implementation instead of using errors.errorString
:
A sophisticated caller can then use a
type assertion to check for a
NegativeSqrtError
and handle it specially, while callers that just
pass the error to fmt.Println
or log.Fatal
will see
no change in behavior.
As another example, the json package specifies a
SyntaxError
type that the json.Decode
function
returns when it encounters a syntax error parsing a JSON blob.
The Offset
field isn't even shown in the default formatting of the
error, but callers can use it to add file and line information to their error
messages:
(This is a slightly simplified version of some actual code from the Camlistore project.)
The error
interface requires only a Error
method;
specific error implementations might have additional methods. For instance, the
net package returns errors of type
error
, following the usual convention, but some of the error
implementations have additional methods defined by the net.Error
interface:
package net type Error interface { error Timeout() bool // Is the error a timeout? Temporary() bool // Is the error temporary? }
Client code can test for a net.Error
with a type assertion and
then distinguish transient network errors from permanent ones. For instance, a
web crawler might sleep and retry when it encounters a temporary error and give
up otherwise.
Simplifying repetitive error handling
In Go, error handling is important. The language's design and conventions encourage you to explicitly check for errors where they occur (as distinct from the convention in other languages of throwing exceptions and sometimes catching them). In some cases this makes Go code verbose, but fortunately there are some techniques you can use to minimize repetitive error handling.
Consider an App Engine application with an HTTP handler that retrieves a record from the datastore and formats it with a template.
{{code "/doc/progs/error2.go" `/func init/` `/STOP/`}}
This function handles errors returned by the datastore.Get
function and viewTemplate
's Execute
method. In both
cases, it presents a simple error message to the user with the HTTP status code
500 ("Internal Server Error"). This looks like a manageable amount of code, but
add some more HTTP handlers and you quickly end up with many copies of
identical error handling code.
To reduce the repetition we can define our own HTTP appHandler
type that includes an error
return value:
Then we can change our viewRecord
function to return errors:
This is simpler than the original version, but the http package doesn't understand functions that return
error
.
To fix this we can implement the http.Handler
interface's
ServeHTTP
method on appHandler
:
The ServeHTTP
method calls the appHandler
function
and displays the returned error (if any) to the user. Notice that the method's
receiver, fn
, is a function. (Go can do that!) The method invokes
the function by calling the receiver in the expression fn(w, r)
.
Now when registering viewRecord
with the http package we use the
Handle
function (instead of HandleFunc
) as
appHandler
is an http.Handler
(not an
http.HandlerFunc
).
With this basic error handling infrastructure in place, we can make it more user friendly. Rather than just displaying the error string, it would be better to give the user a simple error message with an appropriate HTTP status code, while logging the full error to the App Engine developer console for debugging purposes.
To do this we create an appError
struct containing an
error
and some other fields:
Next we modify the appHandler type to return *appError
values:
(It's usually a mistake to pass back the concrete type of an error rather than
error
,
for reasons discussed in the Go FAQ,
but it's the right thing to do here because ServeHTTP
is the only
place that sees the value and uses its contents.)
And make appHandler
's ServeHTTP
method display the
appError
's Message
to the user with the correct HTTP
status Code
and log the full Error
to the developer
console:
Finally, we update viewRecord
to the new function signature and
have it return more context when it encounters an error:
This version of viewRecord
is the same length as the original, but
now each of those lines has specific meaning and we are providing a friendlier
user experience.
It doesn't end there; we can further improve the error handling in our application. Some ideas:
appError
that stores the
stack trace for easier debugging,
appHandler
, logging the error
to the console as "Critical," while telling the user "a serious error
has occurred." This is a nice touch to avoid exposing the user to inscrutable
error messages caused by programming errors.
See the Defer, Panic, and Recover
article for more details.
Conclusion
Proper error handling is an essential requirement of good software. By employing the techniques described in this post you should be able to write more reliable and succinct Go code.