Reputation: 4258
I have the following code:
package main
import (
"fmt"
)
func recoverFoo() {
if r := recover(); r != nil {
println("Recovered")
}
}
func foo() (int, error) {
defer recoverFoo()
panic("shit!")
}
func main() {
x, err := foo()
println("after foo x = " + fmt.Sprint(x))
if err != nil {
println("An error occured")
} else {
println("No error occured")
}
}
In this situtation, I am calling foo (in reality my function foo is calling a third party library which sometimes panics, but also sometimes return err). If it panics I can't have it crashing the app, but I need to know something went wrong as I have to write to local storage on error.
In this case though the value x
returned from Foo can have a valid value of 0. So the recovery setting x and err to their defaults (0 and nil), doesn't tell me if an error actually occurred...
I see two possible solutions, (1) I wrap the err and x into a custom return type and assume if its nil then an error occurred. (2) I have a third return boolean that specifies a panic didn't occur (it will default to false
)
Is their something I'm missing here around go error handling and recovering from panics. I'm new to go so would like some advice.
Upvotes: 2
Views: 1254
Reputation: 44675
Since both the panic and the "soft" error are program exceptions, you should preserve the non-nil error semantics. You can wrap the error in a custom type, or a simple error variable, and check for that after the function call.
Also, in order to actually modify the returned error, you also should:
recover()
in a deferred function literalFrom the specs Defer statements:
For instance, if the deferred function is a function literal and the surrounding function has named result parameters that are in scope within the literal, the deferred function may access and modify the result parameters before they are returned
package main
import (
"errors"
"fmt"
"log"
)
var ErrPanicRecovered = errors.New("recovered from panic")
// named return parameters
func recoverableFoo() (i int, err error) {
defer func() {
if r := recover(); r != nil {
err = fmt.Errorf("%w: %v", ErrPanicRecovered, r)
}
}()
// panic("problem!") // or any call that may panic; uncomment to test
return 1, nil
}
func main() {
x, err := foo()
if err != nil {
if errors.Is(err, ErrPanicRecovered) {
log.Fatal("panicked: ", err)
}
log.Printf("some other error: %s", err.Error())
return
}
fmt.Println("after foo x = " + fmt.Sprint(x))
}
In particular, using fmt.Errorf
with the %w
formatting verb allows you to properly wrap the error and later inspect it with errors.Is
:
If the format specifier includes a
%w
verb with an error operand, the returned error will implement an Unwrap method returning the operand.
Playground: https://play.golang.org/p/p-JI1B0cw3x
Upvotes: 5