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.
#1 Solution for Logs, Traces & Metrics
APM
Kubernetes
Logs
Synthetics
RUM
Serverless
Security
More