But what about errors?

August 22, 2025

Join me today, August 22, at 3:00pm EDT (19:00 UTC), more live coding. Subscribe on YouTube and hit the Notify me button so you don’t miss it!


We’ve been looking at different ways of managing multiple goroutines. But what if one (or more) the functions called in a goroutine might return an error? So far, we don’t have a solution to that in our toolbox.

There are a few different ways we can accomplish this. We’ll discuss a few, starting, today, with using a simple error value and a mutex.

First, a naïve implementation:

func doManyThings() error {
	var wg sync.WaitGroup{}
	var err error

	for i := range 100 {
		wg.Add(1)
		go func() {
			defer wg.Done()
			err = doSomething(i)
		}()
	}

	wg.Wait()
	return err
}

This implementation contains two bugs, which we’ll discuss. But otherwise, it does a pretty good job of conveying the intention:

  1. We declare a variable, in the parent goroutine, to contain our error var err error
  2. The anonymous function in each goroutine sets that err variable with the error returned by doSomething(i)
  3. When all goroutines have completed, we return that error value.

You’ve likely picked up on the obvious error: err = doSomething(i) unconditoinally overrites the error. This means that whichever goroutine completes last, will set the error, potentially ignoring any other errors that happened first. Let’s fix that bug first:

func doManyThings() error {
	var wg sync.WaitGroup{}
	var err error

	for i := range 100 {
		go func() {
			wg.Add(1)
			defer wg.Done()
			if err2 := doSomething(i); err2 != nil && err == nil {
				err = err2
			}
		}()
	}

	wg.Wait()
	return err
}

The key change is:

			if err2 := doSomething(i); err2 != nil && err == nil {
				err = err2
			}

Now we only set the final err value if we receive an actual error from doSomething(i), and when err is nil. In effect, we now report the first error to be encountered. This is fine in many cases. If you want to report all errors that occur, we’ll discuss approaches for that in the future.

The final bug that remains in the existing implementation is more subtle. It’s a datarace. The err variable is not protected, which means it’s possible that two or more goroutines may attempt to read and/or write to it at the same time. While multiple simultaneous reads are safe, multiple writes, or simultaneous reads and writes are not safe!

Enter sync.Mutex.

Consider this updated version:

func doManyThings() error {
	var wg sync.WaitGroup{}
	var mu sync.Mutex
	var err error

	for i := range 100 {
		go func() {
			wg.Add(1)
			defer wg.Done()
			if err2 := doSomething(i); err2 != nil {
				mu.Lock()
				if err == nil {
					err = err2
				}
				mu.Unlock()
			}
		}()
	}

	wg.Wait()
	return err
}

Now, before reading the err value, we establish a lock. Only one active lock is permitted at a time on a sync.Mutex value. If an active lock already exist, any subsequent calls to Lock() will block until they are released. This ensures that at most a single goroutine will read or write the err value at any given time.

After the function is done reading and/or writing the err value, mu.Unlock() releases the lock, so other goroutines may access that variable if needed.


Share this

Direct to your inbox, daily. I respect your privacy .

Unsure? Browse the archive .

Get daily content like this in your inbox!

Subscribe