Errors
Library routines must often return some sort of error indication to the caller. As mentioned earlier, Go’s multivalue return makes it easy to return a detailed error description alongside the normal return value. It is good style to use this feature to provide detailed error information. For example, as we’ll see,os.Open
doesn’t just return anil
pointer on failure, it also returns an error value that describes what went wrong.
By convention, errors have typeerror
, a simple built-in interface.
1 | type error interface { |
A library writer is free to implement this interface with a richer model under the covers, making it possible not only to see the error but also to provide some context. As mentioned, alongside the usual*os.File
return value,os.Open
also returns an error value. If the file is opened successfully, the error will benil
, but when there is a problem, it will hold anos.PathError
:
1 | // PathError records an error and the operation and |
PathError
‘sError
generates a string like this:
1 | open /etc/passwx: no such file or directory |
Such an error, which includes the problematic file name, the operation, and the operating system error it triggered, is useful even if printed far from the call that caused it; it is much more informative than the plain “no such file or directory”.
When feasible, error strings should identify their origin, such as by having a prefix naming the operation or package that generated the error. For example, in packageimage
, the string representation for a decoding error due to an unknown format is “image: unknown format”.
Callers that care about the precise error details can use a type switch or a type assertion to look for specific errors and extract details. ForPathErrors
this might include examining the internalErr
field for recoverable failures.
1 | for try := 0; try < 2; try++ { |
The secondif
statement here is anothertype assertion. If it fails,ok
will be false, ande
will benil
. If it succeeds,ok
will be true, which means the error was of type*os.PathError
, and then so ise
, which we can examine for more information about the error.
Panic
The usual way to report an error to a caller is to return anerror
as an extra return value. The canonicalRead
method is a well-known instance; it returns a byte count and anerror
. But what if the error is unrecoverable? Sometimes the program simply cannot continue.
For this purpose, there is a built-in functionpanic
that in effect creates a run-time error that will stop the program (but see the next section). The function takes a single argument of arbitrary type—often a string—to be printed as the program dies. It’s also a way to indicate that something impossible has happened, such as exiting an infinite loop.
1 | // A toy implementation of cube root using Newton's method. |
This is only an example but real library functions should avoidpanic
. If the problem can be masked or worked around, it’s always better to let things continue to run rather than taking down the whole program. One possible counterexample is during initialization: if the library truly cannot set itself up, it might be reasonable to panic, so to speak.
1 | var user = os.Getenv("USER") |
Recover
Whenpanic
is called, including implicitly for run-time errors such as indexing a slice out of bounds or failing a type assertion, it immediately stops execution of the current function and begins unwinding the stack of the goroutine, running any deferred functions along the way. If that unwinding reaches the top of the goroutine’s stack, the program dies. However, it is possible to use the built-in functionrecover
to regain control of the goroutine and resume normal execution.
A call torecover
stops the unwinding and returns the argument passed topanic
. Because the only code that runs while unwinding is inside deferred functions,recover
is only useful inside deferred functions.
One application ofrecover
is to shut down a failing goroutine inside a server without killing the other executing goroutines.
1 | func server(workChan <-chan *Work) { |
In this example, ifdo(work)
panics, the result will be logged and the goroutine will exit cleanly without disturbing the others. There’s no need to do anything else in the deferred closure; callingrecover
handles the condition completely.
Becauserecover
always returnsnil
unless called directly from a deferred function, deferred code can call library routines that themselves usepanic
andrecover
without failing. As an example, the deferred function insafelyDo
might call a logging function before callingrecover
, and that logging code would run unaffected by the panicking state.
With our recovery pattern in place, thedo
function (and anything it calls) can get out of any bad situation cleanly by callingpanic
. We can use that idea to simplify error handling in complex software. Let’s look at an idealized version of aregexp
package, which reports parsing errors by callingpanic
with a local error type. Here’s the definition ofError
, anerror
method, and theCompile
function.
1 | // Error is the type of a parse error; it satisfies the error interface. |
IfdoParse
panics, the recovery block will set the return value tonil
—deferred functions can modify named return values. It will then check, in the assignment toerr
, that the problem was a parse error by asserting that it has the local typeError
. If it does not, the type assertion will fail, causing a run-time error that continues the stack unwinding as though nothing had interrupted it. This check means that if something unexpected happens, such as an index out of bounds, the code will fail even though we are usingpanic
andrecover
to handle parse errors.
With error handling in place, theerror
method (because it’s a method bound to a type, it’s fine, even natural, for it to have the same name as the builtinerror
type) makes it easy to report parse errors without worrying about unwinding the parse stack by hand:
1 | if pos == 0 { |
Useful though this pattern is, it should be used only within a package.Parse
turns its internalpanic
calls intoerror
values; it does not exposepanics
to its client. That is a good rule to follow.
By the way, this re-panic idiom changes the panic value if an actual error occurs. However, both the original and new failures will be presented in the crash report, so the root cause of the problem will still be visible. Thus this simple re-panic approach is usually sufficient—it’s a crash after all—but if you want to display only the original value, you can write a little more code to filter unexpected problems and re-panic with the original error. That’s left as an exercise for the reader.