Goroutines Error Handling

We have now started measuring AJAX Performance as well, as a part of Web Performance Monitoring.

Handling of AJAX performance report introduces a new problem. Every page that sends report may get a minimum of 30 to N number of AJAX calls (N been unlimited, depending on how Single Page App has been implemented).

We wanted to use goroutines in performance collection process, so that we can leverage the maximum power of golang, by using it awesome concurrency by which we can process all the AJAX performance data in parallel.

What we wanted was to stop all the goroutines when an error has occurred in one of them and capture this failed packet for manual analysis later. Here, I wanted to list the various ways you can handle the errors in goroutines.

Return Variable

Whether can we directly return values from goroutines similar to a normal function? This is a big no-no in the world of concurrency. The reason is, the returns values between goroutines may overwrite the variable that stores the return value. So in the end, you may end up processing only one of the N goroutines! Check out a similar question here in stackoverflow.

var ret int
go func() {
    ret = doSomething()
}()
* ret - will be overwritten by different go routines

Return through Channels

Using a channel to return something is the only way you can guarantee to collect all return values from goroutines without overwriting them. There are multiple ways we can use the same return channels:

  • We can use the return channel to indicate only the error
  • We can use it to return both the result and the error

Separate Channels

An example with separate channels for result and error is below. In this you will notice that the error and the result are sent out in separate channels. You can play it here.

package main

import "fmt"
import "time"

// https://www.atatus.com/blog/ - Goroutines Error Handling
// Example for separate channels for Return and Error

type Result struct {
    ErrorName          string
    NumberOfOccurances int64
}

func getErrorName(errorId string) (<-chan string, <-chan error) {
    names := map[string]string{
        "1001": "a is undefined",
        "2001": "Cannot read property 'data' of undefined",
    }

    out := make(chan string, 1)
    errs := make(chan error, 1)

    go func() {
        time.Sleep(time.Second)
        if name, ok := names[errorId]; ok {
            out <- name
        } else {
            errs <- fmt.Errorf("getErrorName: %s errorId not found", errorId)
        }

        close(out)
        close(errs)
    }()

    return out, errs
}

func getOccurances(errorId string) (<-chan int64, <-chan error) {
    occurances := map[string]int64{
        "1001": 245,
        "2001": 10352,
    }

    out := make(chan int64, 1)
    errs := make(chan error, 1)

    go func() {
        time.Sleep(time.Second)
        if occ, ok := occurances[errorId]; ok {
            out <- occ
        } else {
            errs <- fmt.Errorf("getOccurances: %s errorId not found", errorId)
        }

        close(out)
        close(errs)
    }()

    return out, errs
}

func getError(errorId string) (r *Result, err error) {

    nameOut, nameErr := getErrorName(errorId)
    occurancesOut, occurancesErr := getOccurances(errorId)

    var open bool

    if err, open = <-nameErr; open {
        return
    }
    if err, open = <-occurancesErr; open {
        return
    }
    r = &Result{ErrorName: <-nameOut, NumberOfOccurances: <-occurancesOut}

    return
}

func main() {
    fmt.Println("Using separate channels for error and result")
    errorIds := []string{
        "1001",
        "2001",
        "3001",
    }
    for _, e := range errorIds {
        r, err := getError(e)
        if err != nil {
            fmt.Printf("Failed: %s\n", err.Error())
            continue
        }
        fmt.Printf("Name: \"%s\" has occurred \"%d\" times\n", r.ErrorName, r.NumberOfOccurances)
    }
}

Same Channel

In this, the result and the error are sent in the same channel, which is quite similar to a function returning multiple values. Here, instead of waiting for multiple channels, you wait on a single channel. Play it here.

package main

import "fmt"
import "time"

// https://www.atatus.com/blog/ - Goroutines Error Handling
// Example with same channel for Return and Error

type ResultError struct {
    res Result
    err error
}

type Result struct {
    ErrorName          string
    NumberOfOccurances int64
}

func getError(errorId string) (r ResultError) {
    errors  := map[string]Result {
        "1001": {"a is undefined", 245},
        "2001": {"Cannot read property 'data' of undefined", 10352},
    }
    outputChannel := make (chan ResultError)
    go func() {
        time.Sleep(time.Second)
        if r, ok := errors[errorId]; ok {
            outputChannel <- ResultError{res: r, err: nil}
        } else {
            outputChannel <- ResultError{res: Result{}, err: fmt.Errorf("getErrorName: %s errorId not found", errorId)}
        }
    }()

    return <- outputChannel
}

func main() {
    fmt.Println("Using separate channels for error and result")
    errorIds := []string{
        "1001",
        "2001",
        "3001",
    }
    for _, e := range errorIds {
        r := getError(e)
        if r.err != nil {
            fmt.Printf("Failed: %s\n", r.err.Error())
            continue
        }
        fmt.Printf("Name: \"%s\" has occurred \"%d\" times\n", r.res.ErrorName, r.res.NumberOfOccurances)
    }
}

Conclusion

As you see, whether to use separate channels or same channel depends on your design and need. However, error handling itself from goroutines is of primary importance. This gives your insight of what has happened in this concurrent world of execution.

Justin

Justin

Customer Success Manager at Atatus. Vegan, Runner & Avid Reader.
Bangalore

Monitor your entire software stack

Gain end-to-end visibility of every business transaction and see how each layer of your software stack affects your customer experience.