Practically and efficiently!
While handlings errors in Go is exceptionally annoying, I like the explicit error checks much more than throwing an exception 5 levels up the stack and hoping someone will catch it. I am looking at you Java!
Here are my 5 rules on handling errors in Go.
Rule 1 - Don't ignore the error
Sooner or later your function will fail and you will waste hours figuring why and restoring your program.
Just handle it. If you are in a rush or too tired - take a break.
package main
import (
"fmt"
"time"
)
func main() {
// DO NOT IGNORE THE ERROR
lucky, _ := ifItCanFailItWill()
}
func ifItCanFailItWill() (string, error) {
nowNs := time.Now().Nanosecond()
if nowNs % 2 == 0 {
return "shinny desired value", nil
}
return "", fmt.Errorf("I will fail one day, handle me")
}
Rule 2 - Return early
It may feel natural to focus on the "happy path" of the code execution first, but I prefer to start with validation and return the value at the end when everything went 100% fine.
I don't scale:
func nah() (string, error) {
nowNs := time.Now().Nanosecond()
if nowNs % 2 == 0 && isValid() {
return "shinny desired value", nil
}
return "", fmt.Errorf("I will fail one day, handle me")
}
β PRO:
func earlyReturnRocks() (string, error) {
nowNs := time.Now().Nanosecond()
if nowNs % 2 > 0 {
return "", fmt.Errorf("time dividability must be OCD compliant")
}
if !isValid() {
return "", fmt.Errorf("a different custom, specific, helpful error message")
}
return "shinny desired value", nil
}
Advantages?
- Easier to read
- Easier to add more validation
- Less nested code (especially in loops)
- A clear focus on safety and error handling
- Specific error message per
if
condition possible
Rule 3 - Return value or Error (but not both)
I have seen developers using the return values in combination with an error at the same time. This is a bad practice. Avoid doing this.
Confusing:
func validateToken() (desiredValue string, expiredAt int, err error) {
nowNs := time.Now().Nanosecond()
if nowNs % 2 > 0 {
// THE expiredAt (nowNs) SHOULD NOT BE RETURNED TOGETHER WITH THE ERR
return "", nowNs, fmt.Errorf("token expired")
}
return "shinny desired value", 0, nil
}
Disadvantages?
- Unclear method signature
- One must reverse-engineer the method to know what values are returned and when
You are right, and sometimes you need to return some additional information about the error, in which case, create a new dedicated Error object.
β PRO:
package main
import (
"fmt"
"github.com/davecgh/go-spew/spew"
"time"
)
func main() {
value, err := validateToken()
if err != nil {
spew.Dump(err.Error())
}
spew.Dump(value)
}
// Compatible with error built-in interface.
//
// type error interface {
// Error() string
// }
type TokenExpiredErr struct {
expiredAt int
}
func (e TokenExpiredErr) Error() string {
return fmt.Sprintf("token expired at block %d", e.expiredAt)
}
func validateToken() (desiredValue string, err error) {
nowNs := time.Now().Nanosecond()
if nowNs % 2 > 0 {
return "", TokenExpiredErr{expiredAt: nowNs}
}
return "shinny desired value", nil
}
Rule 4 - Log or Return (but not both)
When you log an error, you are handling it. Do NOT return the error back to the caller - forcing him to handle it as well!
Why? Because you don't want to log the same message twice or more:
package main
import (
"fmt"
"os"
"time"
)
func main() {
// validateToken() is already doing the logging,
// but I didn't reverse engineer the method so I don't know about that
// and now I will unfortunately end up with the same message being logged twice
_, err := validateToken()
if err != nil {
// I have nowhere to return it, SO I RIGHTFULLY LOG IT
// And I will not ignore a possible error writing err
_, err = fmt.Fprint(os.Stderr, fmt.Errorf("validating token failed. %s", err.Error()))
if err != nil {
// Extremely rare, no other choice
panic(err)
}
os.Exit(1)
}
}
type TokenExpiredErr struct {
expiredAt int
}
func (e TokenExpiredErr) Error() string {
return fmt.Sprintf("token expired at block %d", e.expiredAt)
}
func validateToken() (desiredValue string, err error) {
nowNs := time.Now().Nanosecond()
if nowNs % 2 > 0 {
// DO NOT LOG AND RETURN
// DO NOT LOG AND RETURN
// DO NOT LOG AND RETURN
fmt.Printf("token validation failed. token expired at %d", nowNs)
return "", TokenExpiredErr{expiredAt: nowNs}
}
return "shinny desired value", nil
}
Messy output when logging AND returning:
token validation failed. token expired at 115431493validating token failed. token expired at block 115431493
β PRO either logs OR returns:
validating token failed. token expired at block 599480733
Rule 5 - Configure an if err != nil
macro in your IDE
I couldn't keep typing the error check, so I just created a quick video guide I created on how to set it up in GoLand from Intellij. I bound the macro on my Mouse 4 button that I usually use for healing my Necromancer in Guild Wars 2 :)
Do you like Go?
I am writing an eBook on how to build a peer-to-peer system in Go
from scratch!
Check it out: https://web3.coach/#book
I tweet about it at: https://twitter.com/Web3Coach
Top comments (4)
But If logging is handling what should I return? Seems a bit confusing to me not returning an error but an (empty?) value? was this an error or should the value really be just empty?
Hm let me rephrase. You return an error and only log it if there is nothing else u can do with the error. So like on the example. I returned the error up the stack to the main.go bc it was the last place where I could handle it (log it) and then I exited the program.
This is kind of the biggest conundrum. If error happens in component or routine, we usually log the error there since log message can provide additional metadata that caused the problem or help to investigate potential bug. We don;t want to pass those parameters back to main routines to log them there. Its always better to handle/log the error where it occurs.
However after error happens and is logged inside the subrutine, we need to let know the main routine, API endpoint or place the component was called from that processing indeed failed. For that the best mechanism is return nil/default value and error (as even pointed out in this article). Is there other better mechanism to avoid this confusion, log error immediately with metadata/state that happened ?
Unfortunately the very simple example provided here to log in main routine is not always what is happening in more complicated systems with components/packages. I am trying to utilize both, the power of logging the error but also determining the behavior of main application/routine based on nil/err happened.
Very good, thought through question(s) Oscar.
I think you are asking 2 questions right?
1) Why do you think this is helpful? "Its always better to handle/log the error where it occurs."
IMHO, If you can handle an error, no need to log it, maybe as a warning. If you can't handle it, then you log it.
2) "Is there other better mechanism to avoid this confusion, log error immediately with metadata/state that happened ?"
I like to do this:
If I can't handle an error, I create a custom struct, decorated with all the information needed, and return it to the more competent component that can handle it (or can't and logs it).
Let me know if anything is unclear, or you have a concrete example. We can try to "debug it" together.