Converting Panics to Errors in Go Applications
Michael Nikitochkin
Posted on January 23, 2024
Foto von Anton Darius auf Unsplash
Golang is a popular language that makes it easy for beginners to start writing concurrent applications. Often, panics (or exceptions) are processed in the same way as errors, leading to duplicated handling for both. Errors should ideally be handled in a single place to eliminate redundancy in managing panics and errors.
This article explores various techniques to recover from panics in different situations.
In most Golang conventions, functions that return an error
object are commonly used.
Let's explore this with a simple example:
// panic.go
package main
import (
"fmt"
"os"
)
func main() {
err := run()
if err != nil {
fmt.Printf("error: `%+v`\n", err)
os.Exit(1)
}
fmt.Println("success!")
}
func run() error {
return nil
}
Ensure that it works: go run panic.go
Handling Exceptions in a Function
Introduce a bit of distubance and recover from it with defer
and recover
.
func run() error {
+ defer func() {
+ if excp := recover(); excp != nil {
+ fmt.Printf("catch: `%+v`\n", excp)
+ }
+ }()
+
+ fmt.Println("Panicking!")
+ panic("boom!")
+
return nil
}
The program will output:
Panicking!
catch: `boom!`
success
This approach handles unexpected exceptions, but existed with success. However, it's desirable to process errors and exceptions in the main
function in the same way, indicating the presence of an error
.
Using the rule from "Defer, Panic, and Recover"1: 3. Deferred functions may read and assign to the returning function’s named return values.
.
import (
+ "errors"
"fmt"
"os"
)
...
-func run() error {
+func run() (err error) {
defer func() {
if excp := recover(); excp != nil {
fmt.Printf("catch: `%+v`\n", excp)
+ switch v := excp.(type) {
+ case string:
+ err = errors.New(v)
+ case error:
+ err = v
+ default:
+ err = errors.New(fmt.Sprint(v))
+ }
+ }
}()
fmt.Println("Panicking!")
panic("boom!")
- return nil
+ return
}
make sure change the function signature to has a named return value
err error
and return without a value.
The recover()
function returns an interface{}
object. If there's an exception that isn't an error
type, it initializes error
object. Once you have the correct type, assign to err
instance.
This modified program will output:
Panicking!
catch: `boom!`
error: `boom!`
exit status 1
The exit code now indicates that the program finished with error code. As an experiment, you can change the panic argument to other types such as error
or int
.
Handling Exceptions in a Goroutine
Wrap the panic statement with the go
statement and give it some time to allow the scheduler to pick up the goroutine:
import (
"errors"
"fmt"
"os"
+ "time"
)
...
- fmt.Println("Panicking!")
- panic("boom!")
+ blocker := make(chan struct{})
+ go func() {
+ fmt.Println("Panicking!")
+ panic("boom!")
+ blocker <- struct{}{}
+ }()
+
+ select {
+ case <-blocker:
+ panic("this branch should not happen")
+ case <-time.After(time.Second):
+ fmt.Printf("goroutine timeouted! err = `%+v`\n", err)
+ }
return
}
In this case, the panic wasn't caught in the child goroutine:
Panicking!
panic: boom!
goroutine 34 [running]:
main.run.func2()
.../panic.go:124 +0x68
created by main.run in goroutine 1
.../panic.go:122 +0xa4
exit status 2
As a solution, you can duplicate the recovery inside the goroutine function:
blocker := make(chan struct{})
go func() {
+ defer func() {
+ if excp := recover(); excp != nil {
+ fmt.Printf("catch in goroutine: `%+v`\n", excp)
+ switch v := excp.(type) {
+ case string:
+ err = errors.New(v)
+ case error:
+ err = v
+ default:
+ err = errors.New(fmt.Sprint(v))
+ }
+ }
+ }()
+
fmt.Println("Panicking!")
panic("boom!")
blocker <- struct{}{}
Output:
Panicking!
catch in goroutine: `boom!`
goroutine timeouted! err = `boom!`
error: `boom!`
exit status 1
The modified function run
would be as follows:
func run() (err error) {
blocker := make(chan struct{})
go func() {
defer func() {
if excp := recover(); excp != nil {
fmt.Printf("catch in goroutine: `%+v`\n", excp)
switch v := excp.(type) {
case string:
err = errors.New(v)
case error:
err = v
default:
err = errors.New(fmt.Sprint(v))
}
}
}()
fmt.Println("Panicking!")
panic("boom!")
blocker <- struct{}{}
}()
select {
case <-blocker:
panic("this branch should not happen")
case <-time.After(time.Second):
fmt.Printf("goroutine timeouted! err = `%+v`\n", err)
}
return
}
While the resulting code may not be optimal, propagating exceptions for improved processing is crucial. This article explores various Go language tricks, to enhance error handling and exception management in your programs.
I've uploaded the video, along with my clarification, to Vimeo2.
References
Posted on January 23, 2024
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.