Fundamentals 13 min read

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.

Raymond Ops
Raymond Ops
Raymond Ops
Mastering Go Error Handling: From Errors to Panic and Recover

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

error

interface 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.New

from the

errors

package 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.Error

value with a

RuntimeError()

method.

panic

can 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

recover

is used inside a

defer

function to capture a panic value; it returns

nil

if no panic occurred.

Example of a

protect

function that defers

recover

and 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

log

package writes to standard error; fatal functions call

os.Exit(1)

, while panic functions call

panic

after 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

parse

package with a custom

ParseError

type 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

check

function panics on a non‑nil error:

<code>func check(err error) {
    if err != nil {
        panic(err)
    }
}
</code>
errorHandler

returns 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.

GoBest PracticesError Handlingpanicrecovercustom errors
Raymond Ops
Written by

Raymond Ops

Linux ops automation, cloud-native, Kubernetes, SRE, DevOps, Python, Golang and related tech discussions.

0 followers
Reader feedback

How this landed with the community

login Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.