๐ŸŽ“ A1Learner
๐Ÿ”
0 of 28 lessons completed

Go Error Groups

An error group runs a set of jobs together and collects every failure. One job can fail while the rest still finish.

One failure stops the line

Three jobs run one by one here. The second job fails, and the loop stops at once.

package main

import (
	"errors"
	"fmt"
)

func job(id int) error {
	fmt.Println("Job ran:", id)
	if id == 1 {
		return errors.New("job 1 failed")
	}
	return nil
}

func main() {
	for i := 0; i < 3; i++ {
		if err := job(i); err != nil {
			fmt.Println("Err:", err)
			fmt.Println("Stopped early")
			break
		}
	}
	fmt.Println("Main done")
}

Jobs run together in a group

A group starts every job under one WaitGroup. Each job writes one slot, and a failing job sends its error to a buffered channel.

package main

import (
	"fmt"
	"sync"
)

type group struct {
	wg      sync.WaitGroup
	results []int
	errs    chan error
}

func newGroup(n int) *group {
	return &group{results: make([]int, n), errs: make(chan error, n)}
}

func (g *group) start(id int, fail bool) {
	g.wg.Add(1)
	go func() {
		defer g.wg.Done()
		if fail {
			g.errs <- fmt.Errorf("job %d failed", id)
			return
		}
		g.results[id] = (id + 1) * 10
	}()
}

func (g *group) wait() {
	g.wg.Wait()
	close(g.errs)
}

func main() {
	g := newGroup(3)
	g.start(0, false)
	g.start(1, true)
	g.start(2, false)
	g.wait()
	for i, r := range g.results {
		fmt.Printf("Result %d: %d\n", i, r)
	}
	for err := range g.errs {
		fmt.Println("Err:", err)
	}
}

Two failures give a count

Two jobs fail in this group. Main counts the collected errors and never trusts their order.

package main

import (
	"fmt"
	"sync"
)

type group struct {
	wg      sync.WaitGroup
	results []int
	errs    chan error
}

func newGroup(n int) *group {
	return &group{results: make([]int, n), errs: make(chan error, n)}
}

func (g *group) start(id int, fail bool) {
	g.wg.Add(1)
	go func() {
		defer g.wg.Done()
		if fail {
			g.errs <- fmt.Errorf("job %d failed", id)
			return
		}
		g.results[id] = (id + 1) * 10
	}()
}

func (g *group) wait() {
	g.wg.Wait()
	close(g.errs)
}

func main() {
	g := newGroup(3)
	g.start(0, true)
	g.start(1, false)
	g.start(2, true)
	g.wait()
	for i, r := range g.results {
		fmt.Printf("Result %d: %d\n", i, r)
	}
	count := 0
	for range g.errs {
		count++
	}
	fmt.Println("Errors:", count)
}

Example

package main

import (
	"fmt"
	"sync"
)

type group struct {
	wg      sync.WaitGroup
	results []int
	errs    chan error
}

func newGroup(n int) *group {
	return &group{results: make([]int, n), errs: make(chan error, n)}
}

func (g *group) start(id int, fail bool) {
	g.wg.Add(1)
	go func() {
		defer g.wg.Done()
		if fail {
			g.errs <- fmt.Errorf("job %d failed", id)
			return
		}
		g.results[id] = (id + 1) * 10
	}()
}

func (g *group) wait() {
	g.wg.Wait()
	close(g.errs)
}

func main() {
	g := newGroup(4)
	g.start(0, false)
	g.start(1, false)
	g.start(2, true)
	g.start(3, false)
	g.wait()
	for i, r := range g.results {
		fmt.Printf("Result %d: %d\n", i, r)
	}
	for err := range g.errs {
		fmt.Println("Err:", err)
	}
}

Key points

  • An error group runs jobs together and collects what failed.
  • Each job writes its result to its own indexed slot.
  • A sync.WaitGroup joins all jobs before main reads a result.
  • Errors travel through a buffered channel, not a shared variable.
  • The channel closes after the join, so main reads every error.
  • One failure never stops the jobs that still run.

In this example, a group runs four jobs together. Jobs 0, 1 and 3 write 10, 20 and 40 into their slots. Job 2 fails and sends its error to the channel. Main joins the group and prints the slots in job order. The collected error prints last.

Try it Yourself

Edit the code, then press Run.

Running is simulated ยท Go Playground

Exercise

A group runs three jobs and job 1 fails. What does the group report after the join?