Go: Panic & Recover

By George Aristy / llorllale

What are panics?

A secondary flow of control in which the enclosing function stops execution and control is immediately* transferred back to the caller.


* we'll defer details til later 🙂

When does Go panic?

  • Runtime errors: out-of-bounds array accesses, nil pointer dereferences, etc.
  • Invoking panic(): built-in function that starts panicking



https://go.dev/blog/defer-panic-and-recover

Example: nil pointer dereference

                    
func main() {
    var s *string
    _ = *s // nil pointer dereference; program exits immediately
    fmt.Println("Go is awesome!")
}
                    
                
                Output:
                    
panic: runtime error: invalid memory address or nil pointer dereference
[signal SIGSEGV: segmentation violation code=0x1 addr=0x0 pc=0x4553e2]

goroutine 1 [running]:
main.main()
        /home/llorllale/dev/llorllale/demo/talks/golang-panics/go/main.go:5 +0x2

                    
                

Example: Invoking panic()

                    
func main() {
    panic("oh no!") // invoking panic(); program exits immediately
    fmt.Println("Go is awesome!")
}
                    
                
                Output:
                    
panic: oh no!

goroutine 1 [running]:
main.main()
        /home/llorllale/dev/llorllale/demo/talks/golang-panics/go/main.go:11 +0x27
                    
                

Have we seen this before?

Yes! Eg.: Java:
                    
public class Main {
    public static void main(String... args) {
        throw new RuntimeException("oh no!"); // program exits immediately
        System.out.println("Java is awesome!");
    }
}
                    
                
                Output:
                    
Exception in thread "main" java.lang.RuntimeException: oh no!
    at Main.main(Main.java:6)
                    
                

Panics vs. Errors

  • Errors are regular values that flow through ordinary control flow (eg. return)
  • Panics halt ordinary control flow and begin panicking

Can we recover from a panic?

Other languages offer facilities. Eg.: Java:
                
public class Main {
    public static void main(String... args) {
        try {
            throw new RuntimeException("oh no!");
        } catch(RuntimeException e) {
            System.out.println("phew! Managed to recover.");
        }

        System.out.println("Java is awesome!");
    }
}
                
            
            Output:
                
phew! Managed to recover.
Java is awesome!
                
            

Recover

recover() is a built-in function that regains control of a panicking goroutine.


https://go.dev/blog/defer-panic-and-recover

recover(): Cool! Let's try it out!

                
func main() {
    panic("oh no!")
    recover()
    fmt.Println("Go is awesome!")
}
                
            
            Output (not what we want):
                
panic: oh no!

goroutine 1 [running]:
main.main()
        /home/llorllale/dev/llorllale/demo/talks/golang-panics/go/main.go:19 +0x27
                
            

How does recover() work?


TLDR: only deferred functions are run during a panic, so defer your recover()


More info:
  • https://go.dev/ref/spec#Handling_panics
  • https://go.dev/ref/spec#Defer_statements

defer by example

                
func main() {
    defer fmt.Println("deferred function!")
    fmt.Println("Go is awesome!")
}
                
            
            Output:
                
Go is awesome!
deferred function!
                
            

Deferred Recovery: the pattern

                
func main() {
    defer func() { // defer recover()
        if err := recover(); err != nil {
            fmt.Printf("err: %s\n", err)  // handle panic
            fmt.Println("phew! Managed to recover.")
        }
    }()
    panic("oh no!") // code that panics
}
                
            
            Output:
                
err: oh no!
phew! Managed to recover.
                
            

Panics unwind the call stack

                
func main() {
    foo()
}

func foo() {
    bar()
}

func bar() {
    funcThatPanics()
}

func funcThatPanics() {
    panic("oh no!")
}
                
            

Panics unwind the call stack

            Output:
                
panic: oh no!

goroutine 1 [running]:
main.funcThatPanics(...)
        /home/llorllale/dev/llorllale/demo/talks/golang-panics/go/main.go:52
main.bar(...)
        /home/llorllale/dev/llorllale/demo/talks/golang-panics/go/main.go:48
main.foo(...)
        /home/llorllale/dev/llorllale/demo/talks/golang-panics/go/main.go:44
main.main()
        /home/llorllale/dev/llorllale/demo/talks/golang-panics/go/main.go:40 +0x29
                
            

Recover at any point in a goroutine's call stack

                
func main() {
    fmt.Println("entering main")
    foo()
    fmt.Println("exit main")
}

func foo() {
    fmt.Println("entering foo")
    defer func() {
        if err := recover(); err != nil {
            fmt.Println(err)
            fmt.Println("phew! Managed to recover.")
        }
    }()
    bar()
    fmt.Println("exit foo")
}

func bar() {
    fmt.Println("entering bar")
    funcThatPanics()
    fmt.Println("exit bar")
}

func funcThatPanics() {
    fmt.Println("entering funcThatPanics")
    panic("oh no!")
    fmt.Println("exit funcThatPanics")
}
                
            

Recover at any point in a goroutine's call stack

            Output:
                
entering main
entering foo
entering bar
entering funcThatPanics
oh no!
phew! Managed to recover.
exit main
                
            

Unhandled panics exit the program

Recall that panics are handled per goroutine:

func main() {
    defer func() {
        if err := recover(); err != nil { // ineffective
            fmt.Println("phew! Managed to recover.")
        }
    }()

    fmt.Println("enter main")

    done := make(chan struct{})

    go func() {
        funcThatPanics() // unhandled panic in goroutine
    }()

    <-done

    fmt.Println("exit main")
}

func funcThatPanics() {
    fmt.Println("enter funcThatPanics")
    panic("oh no!")
    fmt.Println("exit funcThatPanics")
}
            

Unhandled panics exit the program

Output:

enter main
enter funcThatPanics
panic: oh no!

goroutine 7 [running]:
main.funcThatPanics()
        /home/llorllale/dev/llorllale/demo/talks/golang-panics/go/main.go:95 +0x65
main.main.func2()
        /home/llorllale/dev/llorllale/demo/talks/golang-panics/go/main.go:85 +0x17
created by main.main
        /home/llorllale/dev/llorllale/demo/talks/golang-panics/go/main.go:84 +0x86
            

Other languages panic a bit differently

Eg. Java:

public class Main {
  public static void main(String... args) throws InterruptedException {
    System.out.println("enter main");
    var done = new ArrayBlockingQueue(1);
    new Thread(Main::funcThatErrors).start();
    done.take(); // program hangs here
    System.out.println("exit main");
  }

  private static void funcThatErrors() {
    throw new Error("oh no!");
  }
}
            

Other languages panic a bit differently

Output (program still running):

enter main
Exception in thread "Thread-0" java.lang.Error: oh no!
    at Main.funcThatErrors(Main.java:39)
    at java.base/java.lang.Thread.run(Thread.java:833)
            

Throw anything at panic and recover it


func main() {
    type aThing struct {
        This string
        That int
    }

    defer func() {
        if err := recover(); err != nil {
            switch err.(type) {
            case aThing:
                fmt.Printf("I caught a thing: %+v\n", err)
            default:
                fmt.Println("oops - I don't know what I caught")
            }
        }
    }()

    panic(aThing{
        This: "this is a thing!",
        That: 42,
    })
}
            

Throw anything at panic and recover it

Output:

I caught a thing: {This:this is a thing! That:42}
            

Best Practices

Don't panic.




Go Proverb: https://go-proverbs.github.io/

But if you do...

Don't expose panics to clients. Think hard before you do this:

func doSomething() (err error) {
    defer func() {
        err = recover()
    }()

    doStep1()
    doStep2()
    doStep3()
    doStep4()
    doStep5()

    return
}

func doStepN() {
    ...
    if err != nil {
        panic(err)
    }
    ...
    if done {
        panic(nil)
    }
}
            



https://go.dev/doc/effective_go#recover

Avoid GOTO with panics

Don't do this 😭:

func main() {
    n := func () (result int)  {
        defer func() {
            if v := recover(); v != nil {
                if n, ok := v.(int); ok {
                    result = n
                }
            }
        }()

        func () {
            func () {
                func () {
                    // ...
                    panic(123) // panic on succeeded
                }()
                // ...
            }()
        }()
        // ...
        return 0
    }()
    fmt.Println(n) // 123
}
            

Recover from goroutines

Use defer/recover for goroutines to guard against panics:

func main() {
    done := make(chan struct{})

    for range []int{1, 2, 3} {
        go func() {
            defer func() {
                if err := recover(); err != nil {
                    fmt.Println("phew! Managed to recover.")
                }
            }()
            funcThatMayPanic()
        }()
    }

    <-done
}

func funcThatMayPanic() {
    panic("oh no!")
}