Home - Teleport Blog - Error Handling in Go Programs - Nov 1, 2015
Error Handling in Go Programs
In this post we're going to share some lessons we've learned while using Go in production for several years here at Teleport.
Intro
Go does not have exceptions, rather error handling is done by checking errors. This requires some adjustments in how you deal with errors if you are coming from languages that have exceptions as a primary error handling mechanism.
Dave Cheney wrote a series of articles going deeper into better ways to handle errors in Go, which I highly recommend reading.
Here are some tips and solutions that helped me to improve error handling in Go applications:
Avoiding error shadowing
I found it important not to shadow error value by adding additional information to it so you can inspect it later:
if err != nil {
// trying to add useful information and add context to
// the error that occurred:
return fmt.Errorf("error when processing request %v %v: %v",
req.Method, req.URL, err)
}
The problem with the approach above is that it shadows the original error by creating a new object, so it would be impossible to do advanced handling:
if os.IsNotExist(err) { // would not work if fmt.Errorf was used
// do smart things here
}
One possible alternative to this is to add logging in the error handling block:
if err != nil {
log.Errorf("error when processing request %v %v: %v", req.Method, req.URL, err)
return err
}
This will definitely help to troubleshoot production applications. When a
customer pings you about that 500 Bad request
response, you are able to check
the logs and at the same time you still have an access to the original error.
Unfortunately, using logging in error handlers makes the application code look
very verbose. It will be filled with endless log.Errorf
or log.Infof
entries - most likely the same message will be repeated multiple times.
Getting stack traces back
One thing I miss in exceptions is the useful stack traces that help to understand the origins of error and simplify troubleshooting in production.
Go actually has a way to get a full stack trace, and it is used in standard log library to report the line and file of the code.
The function runtime.Caller helps us to know exactly which line originated the log message:
Caller reports file and line number information about function invocations on
the calling goroutine's stack.
Putting it all together
Package trace is an attempt to combine all these ideas: avoid excessive logging and preserve the original error and its origin for production troubleshooting.
Here's how it works by example. Let us define some errors that are application-specific:
package errors
import (
"fmt"
"github.com/gravitational/trace"
)
// NotFoundError is returned when database storage backend failed
// to find the object by given id
type NotFoundError struct {
// Traces keeps track of files and lines in code
// where trace.Wrap(err) was called, accumulating "stack trace"
// and adds some methods for trace.Wrap to use
trace.Traces
ID string
Message string
}
// Error returns error description and some debugging information
func (n *NotFoundError) Error() string {
return fmt.Sprintf(
"NotFoundError(%v,id=%v,message=%v)",
n.Traces, // this will output the stack traces
n.ID, n.Message)
}
// OrigError tells trace package how to get to the original error, in this case
// this is the error itself
func (n *NotFoundError) OrigError() error {
return n
}
// IsNotFound will help us to do error handling by asserting behaviour
// http://dave.cheney.net/2014/12/24/inspecting-errors
func (n *NotFoundError) IsNotFound() bool {
return true
}
Our SQL code can now use it like this:
// somewhere in internals of our SQL handling code
if err == sql.ErrNoRows {
return trace.Wrap(&storage.NotFoundError{
Type: "account",
ID: name,
})
}
trace.Wrap
will see an error that inherits the trace.Traces
methods, and
will simply add the trace entry to the error:
if s, ok := err.(TraceSetter); ok {
s.SetTrace(t.Trace)
return s
}
The error handling logic benefits from inspecting the original error and seeing the full stack trace with the origins of error:
error in WithTransaction: [suite.go:173 main.go:12] file 'sqlite.db' is missing
What does trace
do when it does not see the TraceSetter
interface (e.g., in
case of system error)? In this case it simply wraps the error and makes it
accessible via its OrigError
method. That's a bit verbose, but sometimes can
be a good compromise for preserving debugging information and keeping the
original error intact.
Teleport cybersecurity blog posts and tech news
Every other week we'll send a newsletter with the latest cybersecurity news and Teleport updates.
trace.Errorf
Package trace
has a couple of other useful functions, that can be helpful for
quick debugging, one of them is trace.Errorf
:
// one can use it instead of fmt.Errorf, works the same but preserves
// line and file where it was called
return trace.Errorf("some problem occurred: %v")
Thanks for reading and hope this was helpful!
We are always looking for better ways to do things at Teleport. Let us
know how you are handling errors in you Go code, drop us a line at
[email protected]
and/or sign up for the updates from our blog below.
Tags
Teleport Newsletter
Stay up-to-date with the newest Teleport releases by subscribing to our monthly updates.