Mastering Error and Exception Handling in Go: Best Practices and Patterns
This article explains the distinction between errors and exceptions in Go, outlines when to use error returns versus panic/recover, and provides a series of practical patterns with code examples to write clearer, more maintainable backend code.
1. Introduction
Errors and exceptions are often confused; many developers treat every abnormal situation as an error and ignore the concept of exceptions. In Go, the language follows a "less is more" philosophy, only providing exception‑like mechanisms (panic/recover) when they add clear value.
2. Fundamentals
An error represents a problem that was expected, such as a failed file open, while an exception (panic) represents an unexpected condition, like a nil‑pointer dereference. Go uses the error interface for standard error handling, returning it as the last value in a function signature. The built‑in functions panic and recover trigger and handle exceptions, and defer postpones execution of a function until the surrounding function returns, regardless of whether it returns normally or via panic.
Errors and exceptions can be converted: an operation that fails repeatedly can promote an error to a panic, and a recovered panic can be turned back into an error for upstream handling.
3. Insight
In the regexp package, Compile returns (*Regexp, error) and is suitable for user‑provided patterns, while MustCompile panics on invalid input and is intended for hard‑coded patterns. The key takeaway is to define clear rules for when to express a problem as an error versus an exception, avoiding a "everything is an error" or "everything is an exception" approach.
4. Correct Practices
Practice 1: Use a boolean when there is only one failure reason
func (self *AgentContext) CheckHostType(host_type string) error {
switch host_type {
case "virtual_machine":
return nil
case "bare_metal":
return nil
}
return errors.New("CheckHostType ERROR:" + host_type)
}
func (self *AgentContext) IsValidHostType(hostType string) bool {
return hostType == "virtual_machine" || hostType == "bare_metal"
}If a function can fail for only one reason, return a bool instead of an error. When multiple failure reasons exist, keep returning error.
Practice 2: Omit error when there is no failure
func (self *CniParam) setTenantId() {
self.TenantId = self.PodNs
}
self.setTenantId() // no error handling neededPractice 3: Place error as the last return value
resp, err := http.Get(url)
if err != nil {
return nil, err
}
value, ok := cache.Lookup(key)
if !ok {
// handle missing value
}Practice 4: Define error constants centrally
var ERR_EOF = errors.New("EOF")
var ERR_CLOSED_PIPE = errors.New("io: read/write on closed pipe")
var ERR_NO_PROGRESS = errors.New("multiple Read calls return no data or error")
var ERR_SHORT_BUFFER = errors.New("short buffer")
var ERR_SHORT_WRITE = errors.New("short write")
var ERR_UNEXPECTED_EOF = errors.New("unexpected EOF")Practice 5: Log at every layer when propagating errors
Adding logs at each layer simplifies fault diagnosis.
Practice 6: Use defer for cleanup on error paths
func deferDemo() error {
err := createResource1()
if err != nil {
return ERR_CREATE_RESOURCE1_FAILED
}
defer func() {
if err != nil {
destroyResource1()
}
}()
// repeat for other resources
return nil
}Practice 7: Retry transient failures instead of returning immediately
When failures are occasional, retry the operation a limited number of times with back‑off before propagating an error.
Practice 8: Do not return error from cleanup functions that callers ignore
For functions like destroy or clear, log the error internally and omit it from the signature.
Practice 9: Preserve useful return values when an error occurs
If a function returns useful data alongside a non‑nil error (e.g., Read returns bytes read), handle both values.
5. Exception Handling Practices
Practice 1: Fail fast during development
Use panic to surface bugs early, ensuring they are noticed and fixed promptly.
Practice 2: Recover in production to avoid process termination
Wrap top‑level goroutine code with a deferred recover that logs the stack and converts the panic into an error so the program can continue safely.
func funcA() (err error) {
defer func() {
if p := recover(); p != nil {
fmt.Printf("panic recover! p: %v", p)
debug.PrintStack()
if str, ok := p.(string); ok {
err = errors.New(str)
} else {
err = errors.New("panic")
}
}
}()
return funcB()
}Practice 3: Use panic for impossible branches
switch s := suit(drawCard()); s {
case "Spades":
// ...
case "Hearts":
// ...
case "Diamonds":
// ...
case "Clubs":
// ...
default:
panic(fmt.Sprintf("invalid suit %v", s))
}Practice 4: Panic when input should never be invalid (hard‑coded scenarios)
func MustCompile(str string) *Regexp {
re, err := Compile(str)
if err != nil {
panic("regexp: Compile(`" + quote(str) + "`): " + err.Error())
}
return re
}6. Conclusion
The article uses Go as an example to clarify the difference between errors and exceptions and presents a collection of practical handling patterns that can be applied individually or combined to improve code readability and maintainability.
Go Development Architecture Practice
Daily sharing of Golang-related technical articles, practical resources, language news, tutorials, real-world projects, and more. Looking forward to growing together. Let's go!
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
