Stuart
Stuart

Reputation: 4258

Distinguish between panic with recovery and no error occuring

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

Answers (1)

blackgreen
blackgreen

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:

  • use recover() in a deferred function literal
  • use named return parameters

From 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

Related Questions