Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to catch runtime error from a function invoked from a waitgroup?

Tags:

concurrency

go

How to handle crashes in a waitgroup gracefully?

In other words, in the following snippet of code, how to catch the panics/crashes of goroutines invoking method do()?

func do(){
    str := "abc"
    fmt.Print(str[3])
    defer func() {
        if err := recover(); err != nil {
            fmt.Print(err)
        }
    }()
}

func main() {
    var wg sync.WaitGroup

    for i := 0; i < 1; i++ {
        wg.Add(1)
        go do()
        defer func() {
            wg.Done()
            if err := recover(); err != nil {
                fmt.Print(err)
            }
        }()
    }
    wg.Wait()
    fmt.Println("This line should be printed after all those invocations fail.")
}
like image 368
Fallen Avatar asked Oct 18 '22 20:10

Fallen


1 Answers

First, registering a deferred function to recover should be the first line in the function, as since you do it last, it won't even be reached because the line / code before the defer already panics and so the deferred function does not get registered which would restore the panicing state.

So change your do() function to this:

func do() {
    defer func() {
        if err := recover(); err != nil {
            fmt.Println("Restored:", err)
        }
    }()
    str := "abc"
    fmt.Print(str[3])
}

Second: this alone will not make your code work, as you call wg.Defer() in a deferred function which would only run once main() finishes - which is never because you call wg.Wait() in your main(). So wg.Wait() waits for the wg.Done() calls, but wg.Done() calls will not be run until wg.Wait() returnes. It's a deadlock.

You should call wg.Done() from the do() function, in the deferred function, something like this:

var wg sync.WaitGroup

func do() {
    defer func() {
        if err := recover(); err != nil {
            fmt.Println(err)
        }
        wg.Done()
    }()
    str := "abc"
    fmt.Print(str[3])
}

func main() {
    for i := 0; i < 1; i++ {
        wg.Add(1)
        go do()
    }
    wg.Wait()
    fmt.Println("This line should be printed after all those invocations fail.")
}

Output (try it on the Go Playground):

Restored: runtime error: index out of range
This line should be printed after all those invocations fail.

This of course needed to move the wg variable to global scope. Another option would be to pass it to do() as an argument. If you decide to go this way, note that you have to pass a pointer to WaitGroup, else only a copy will be passed (WaitGroup is a struct type) and calling WaitGroup.Done() on a copy will not have effect on the original.

With passing WaitGroup to do():

func do(wg *sync.WaitGroup) {
    defer func() {
        if err := recover(); err != nil {
            fmt.Println("Restored:", err)
        }
        wg.Done()
    }()
    str := "abc"
    fmt.Print(str[3])
}

func main() {
    var wg sync.WaitGroup
    for i := 0; i < 1; i++ {
        wg.Add(1)
        go do(&wg)
    }
    wg.Wait()
    fmt.Println("This line should be printed after all those invocations fail.")
}

Output is the same. Try this variant on the Go Playground.

like image 171
icza Avatar answered Oct 22 '22 01:10

icza