Error handling in concurrent programs in Golang


Error handling in concurrent programs in Go consists of little more work than

if err != nil {
   return err
}

because the return value doesn’t reach intended receiver (for example in parent function where function was fired as a goroutine using go keyword). Just as we use channel for sending resulting data, we must also use channel for sending error. And we can do this using single channel of type Data which is a struct that will hold data and error for the data. And than when we range over channel we first check if there’s an error (and when we are done, when all tasks have been completed, we close the channel).

Using same channel for data and error simplifies concurrency, we don’t have to handle scenarios of completed tasks end error handling separately.

So here’s a simple example of what I have in mind (with sending HTTP request as a concurrent task).

type Data struct {
    URL      string
    IsAvailable bool
    Err      error
}

func getAvailableSites(urls []string) ([]string, error) {
    var wg sync.WaitGroup
    urlChan := make(chan *Data)

    go func() {
        wg.Wait()
        close(urlChan)
    }()

    for _, url := range urls {
        go checkIfSiteAvailable(&wg, urlChan, url)
    }

    availableSites := []string{}
    for data := range urlChan {
        if data.Err != nil {
            return nil, fmt.Errorf("problem getting url: %v", data.Err)
        }
        if (data.IsAvailable) {
            availableSites = append(availableSites, data.URL)
        }
    }
    return availableSites, nil
}

func checkIfSiteAvailable(wg *sync.WaitGroup, urlChan chan<- *Data, url string) {
    defer wg.Done()

    resp, err := http.Head(url)
    if err != nil {
        urlChan <- &Data{Err: err}
        return
    }

    if resp.StatusCode != http.StatusOK {
        urlChan <- &Data{Err: fmt.Errorf("status code not OK for %s", url)}
        return
    }

    urlChan <- &Data{URL: url, Available: true}
}

I won’t be describing logic here just a steps related to concurrency and error handling in it. In getAvailableSites we

  • set up a channel we’ll be using for sending the data for specific URL (with error as part of data),
  • a WaitGroup to specify how many goroutines we’ll fire and after all finish, Go will know to close channel,
  • define and fire a goroutine which waits for all goroutines to finish and than close a channel,
  • for each URL we call a function checkIfSiteAvailable as a goroutine (alonside URL we pass channel so we can send resulting data to it and a WaitGroup to be able to indicate task is finished)
  • iterate over a channel, which will finish after we close the channel (from 3rd point). Iterating over a channel is a blocking operation so we don’t have to worry data from channel will get lost. In for loop we check for error and return it as if no concurrency was involved.

In checkIfSiteAvailable we defer call to wg.Done to indicate task is finished. And whenever there’s an error we send instance of Data with only Err field specified because in this simple example we don’t need other fields. Lastly, we send the actual data (wrapped in the Data struct).