Where to log error in go

23.3k views Asked by At

Perhaps this is an opinion or maybe it's actually a best practice, but I want to do it correctly.

Consider the following code:

func main() {
    if err := doSomething(); err != nil {
        // log here and exit?
    }
}

func doSomething() {
    f, err := os.Open("filename.ext")
    if err != nil {
        // log here and return the error/exit?
    }
}

I am interested to know where the error should be logged and where the program should be exited (assuming recovery is impossible). Some possibilities include: logging and exiting in the callee; logging in the callee, returning the error, and exiting in the caller; logging in the callee, returning the error, logging in the caller, and exiting. There seem to be benefits to all of these approaches. The second approach, for instance, allows a fine grained error message but still passes the error on to the caller. However, it will result in two log messages.

Thanks!

2

There are 2 answers

1
captncraig On

This is an interesting topic, and really boils down to a lot of opinion, so this may get closed. There are a few guidelines I try to follow:

  1. Only "handle" an error once. Logging counts as handling. If you do not know what action needs to be taken, just pass it up. You definitely don't want errors to be logged multiple times per occurrence.

  2. If your caller is likely to change their behavior because of the error, you should return it. If you can still do your "job" despite the error, maybe you should log it and continue.

  3. Sometimes it helps to add context to where the error initially entered your system (so the log line can include where the error came from, not just where it finally got logged). Something like https://github.com/pkg/errors can be useful for this.

0
Dhananjay On

From unit-tests standpoint, it's better to return error. If you return error for a fn(), it's easier to unit-test it.

For example,

// p.go
func fn() error {
    ...
}

// p_test.go
func TestFn(t *testing.T) {
    // returning error makes easier to test fn() here.
    if err := fn(); err != nil {
        t.Error("fn(): got:%v, want:nil", err)
    }
}

If fn() directly logged error and exited, it would be impossible to unit-test it.