Mastering Go Error Handling: From Errors to Panic and Recover
This article explains Go's error handling conventions, including returning error values, creating custom errors with the errors package, using fmt.Errorf, handling runtime panics, and recovering from them with defer, plus advanced patterns like panic‑recover wrappers and closure‑based error management, illustrated with comprehensive code examples.
Error
Returning an error object as the sole or last return value—if the value is nil, no error occurred—and the calling function must always check the received error.
Handle errors and return error information to the user; panic and recover are used for true exceptions.
To prevent a function (or the whole program) from being aborted when an error occurs, the error must be checked after calling the function.
<code>if value, err := pack1.Func1(param1); err != nil {
fmt.Printf("Error %s in pack1.Func1 with parameter %v", err.Error(), param1)
// return or return err
}
// Process(value)</code>Error Handling
Go has a predefined
errorinterface type.
<code>type error interface {
Error() string
}</code>Error values represent abnormal states; the program can terminate with
os.Exit(1)when in an error state.
Defining Errors
Whenever a new error type is needed, use
errors.Newfrom the
errorspackage to create one with an appropriate message.
<code>// errors.go
package main
import (
"errors"
"fmt"
)
var errNotFound error = errors.New("Not found error")
func main() {
fmt.Printf("error: %v", errNotFound)
}
// output: error: Not found error</code>Use it in a square‑root function example:
<code>func Sqrt(f float64) (float64, error) {
if f < 0 {
return 0, errors.New("math - square root of negative number")
}
// implementation of Sqrt
}
if f, err := Sqrt(-1); err != nil {
fmt.Printf("Error:%s\n", err)
}
</code>Create richer messages with
fmt.Errorf:
<code>if f < 0 {
return 0, fmt.Errorf("math: square root of negative number %g", f)
}
</code>Command‑line help error example:
<code>if len(os.Args) > 1 && (os.Args[1] == "-h" || os.Args[1] == "--help") {
err = fmt.Errorf("usage: %s infile.txt outfile.txt", filepath.Base(os.Args[0]))
return
}
</code>Runtime Panic
Runtime panics occur on out‑of‑bounds array access, failed type assertions, etc., producing a
runtime.Errorvalue with a
RuntimeError()method.
paniccan be invoked directly for unrecoverable conditions; it prints the provided value and aborts the program.
<code>package main
import "fmt"
func main() {
fmt.Println("Starting the program")
panic("A severe error occurred: stopping the program!")
fmt.Println("Ending the program")
}
</code>Output shows the panic stack trace.
Recover from Panic
recoveris used inside a
deferfunction to capture a panic value; it returns
nilif no panic occurred.
Example of a
protectfunction that defers
recoverand logs the panic:
<code>func protect(g func()) {
defer func() {
log.Println("done")
if err := recover(); err != nil {
log.Printf("run time panic: %v", err)
}
}()
log.Println("start")
g()
}
</code>The
logpackage writes to standard error; fatal functions call
os.Exit(1), while panic functions call
panicafter logging.
<code>// panic_recover.go
package main
import "fmt"
func badCall() {
panic("bad end")
}
func test() {
defer func() {
if e := recover(); e != nil {
fmt.Printf("Panicing %s\r\n", e)
}
}()
badCall()
fmt.Printf("After bad call\r\n") // not reached
}
func main() {
fmt.Printf("Calling test\r\n")
test()
fmt.Printf("Test completed\r\n")
}
</code>Running this program prints the recovered panic message and then continues.
Custom Package Error Handling and Panicking
Best practice for package authors: recover from internal panics within the package and return errors to callers; do not let panics escape the package boundary.
Example
parsepackage with a custom
ParseErrortype that panics on invalid input and recovers in
Parse:
<code>type ParseError struct {
Index int
Word string
Err error
}
func (e *ParseError) String() string {
return fmt.Sprintf("pkg parse: error parsing %q as int", e.Word)
}
func Parse(input string) (numbers []int, err error) {
defer func() {
if r := recover(); r != nil {
if e, ok := r.(error); ok {
err = e
} else {
err = fmt.Errorf("pkg: %v", r)
}
}
}()
fields := strings.Fields(input)
numbers = fields2numbers(fields)
return
}
func fields2numbers(fields []string) (numbers []int) {
if len(fields) == 0 {
panic("no words to parse")
}
for idx, field := range fields {
num, err := strconv.Atoi(field)
if err != nil {
panic(&ParseError{idx, field, err})
}
numbers = append(numbers, num)
}
return
}
</code>Using the package:
<code>var examples = []string{
"1 2 3 4 5",
"100 50 25 12.5 6.25",
"2 + 2 = 4",
"1st class",
"",
}
for _, ex := range examples {
fmt.Printf("Parsing %q:\n ", ex)
nums, err := parse.Parse(ex)
if err != nil {
fmt.Println(err)
continue
}
fmt.Println(nums)
}
</code>Output shows successful parses and detailed error messages for invalid inputs.
Closure‑Based Error Handling Pattern
When all functions share the same signature, defer/panic/recover can be wrapped in a higher‑order function to eliminate repetitive error checks.
Helper
checkfunction panics on a non‑nil error:
<code>func check(err error) {
if err != nil {
panic(err)
}
}
</code> errorHandlerreturns a wrapped function that recovers and logs any panic:
<code>func errorHandler(fn fType1) fType1 {
return func(a type1, b type2) {
defer func() {
if e, ok := recover().(error); ok {
log.Printf("run time panic: %v", e)
}
}()
fn(a, b)
}
}
</code>Inside functions, replace repetitive
if err != nil { … }blocks with
check(err)calls.
Raymond Ops
Linux ops automation, cloud-native, Kubernetes, SRE, DevOps, Python, Golang and related tech discussions.
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.