No description
Find a file
2025-11-14 15:53:01 -06:00
.gitignore Implement core waiterr package with initial functionality and tests. 2025-11-13 14:01:30 -06:00
.golangci.yaml ⚙️ Update Go version and linter exclusions for test files. 2025-11-13 14:26:49 -06:00
AGENTS.md 📝 Update AGENTS.md with new sync.WaitGroup guidelines. 2025-11-13 14:38:43 -06:00
CHANGELOG.md 🔖 Release v1.0.0 2025-11-14 15:26:21 -06:00
CONTRIBUTING.md 📝 Add CONTRIBUTING.md for human contributors 2025-11-14 15:22:12 -06:00
example_test.go 📝 Rename example tests for godoc visibility 2025-11-14 15:11:26 -06:00
go.mod ⚙️ Update Go version and linter exclusions for test files. 2025-11-13 14:26:49 -06:00
go.sum Implement core waiterr package with initial functionality and tests. 2025-11-13 14:01:30 -06:00
LICENSE 📄 Add MIT License 2025-11-13 14:47:36 -06:00
README.md 📝 Add Alternatives section to README.md 2025-11-14 15:53:01 -06:00
Taskfile.yml Initial project setup with basic scaffolding, build tools, and agent guidelines. 2025-11-13 11:52:56 -06:00
waiterr.go Add WithContext function for cancellation 2025-11-14 14:53:36 -06:00
waiterr_test.go Add WithContext function for cancellation 2025-11-14 14:53:36 -06:00

waiterr

waiterr is a Go package that wraps sync.WaitGroup with enhanced error handling capabilities. It allows you to run multiple goroutines, collect any errors they return, and wait for their completion, either returning the first error encountered or aggregating all errors.

Features

  • Go(f func() error): Runs a function f in a new goroutine, storing any error it returns.
  • Wait() error: Waits for all goroutines to complete and returns a combined error of all non-nil errors.
  • WaitForError() error: Waits for the first error to be returned by any goroutine and immediately returns that error. If all goroutines complete without error, it returns nil.
  • Unwrap() []error: Returns a slice of all non-nil errors encountered by the goroutines.

Installation

To install waiterr, use go get:

go get codeberg.org/danjones000/waiterr

Usage

Here's a basic example of how to use waiterr:

package main

import (
	"errors"
	"fmt"
	"time"

	"codeberg.org/danjones000/waiterr"
)

func main() {
	we := waiterr.New()

	we.Go(func() error {
		time.Sleep(100 * time.Millisecond)
		fmt.Println("Goroutine 1 finished")
		return nil
	})

	we.Go(func() error {
		time.Sleep(50 * time.Millisecond)
		fmt.Println("Goroutine 2 finished with an error")
		return errors.New("something went wrong in goroutine 2")
	})

	we.Go(func() error {
		time.Sleep(150 * time.Millisecond)
		fmt.Println("Goroutine 3 finished")
		return nil
	})

	// Wait for all goroutines and get all errors
	if err := we.Wait(); err != nil {
		fmt.Printf("All goroutines finished. Combined error: %v
", err)
	}

	// You can also get the first error immediately
	we2 := waiterr.New()
	we2.Go(func() error {
		time.Sleep(100 * time.Millisecond)
		return errors.New("first error from we2")
	})
	we2.Go(func() error {
		time.Sleep(50 * time.Millisecond)
		return errors.New("second error from we2")
	})

	if err := we2.WaitForError(); err != nil {
		fmt.Printf("First error from we2: %v
", err)
	}

	// Get all unwrapped errors
	unwrappedErrors := we.Unwrap()
	if len(unwrappedErrors) > 0 {
		fmt.Println("Unwrapped errors:")
		for i, err := range unwrappedErrors {
			fmt.Printf("  %d: %v
", i+1, err)
		}
	}
}

Using WithContext

package main

import (
	"context"
	"fmt"
	"time"

	"codeberg.org/danjones000/waiterr"
)

func main() {
	ctx, cancel := context.WithCancel(context.Background())
	defer cancel()

	we, ctx := waiterr.WithContext(ctx)

	we.Go(func() error {
		select {
		case <-time.After(100 * time.Millisecond):
			fmt.Println("Task completed")
			return nil
		case <-ctx.Done():
			fmt.Println("Task cancelled")
			return ctx.Err()
		}
	})

	_ = we.Wait()
	// Output:
	// Task completed
}

Alternatives

waiterr provides a focused approach to goroutine error handling. Here's how it compares to other common Go concurrency primitives and libraries:

  • sync.WaitGroup:

    • Differences: sync.WaitGroup is a fundamental primitive for waiting for a collection of goroutines to finish. It does not, however, provide any built-in mechanisms for error propagation or collection. waiterr builds upon sync.WaitGroup by adding robust error handling, allowing you to collect all errors, or return the first error encountered.
  • golang.org/x/sync/errgroup.Group:

    • Differences: errgroup.Group is similar to waiterr in that it allows you to run goroutines and collect errors. errgroup.Group provides a rate-limiting mechanism, to limit the number of goroutines, which waiterr does not provide, as it is intended to be more light-weight. Additionally, waiterr provides an Unwrap method to retrieve all non-nil errors. Finally waiterr.WaitErr.Wait returns a combination of all errors, wheres errgroup.Group.Wait only returns the first found error.
  • gopkg.in/tomb.v2.Tomb:

    • Differences: tomb.Tomb is a more comprehensive library for managing the lifecycle of goroutines, including graceful shutdown, error propagation, and child goroutine management. waiterr is a lighter-weight alternative focused specifically on waiting for goroutines and collecting their errors, without the full lifecycle management capabilities of tomb.Tomb. If you need advanced goroutine supervision and cleanup, tomb.Tomb might be a better fit. For simpler error aggregation and waiting, waiterr offers a more streamlined solution.

Contributing

Please refer to the CONTRIBUTING.md file for guidelines on contributing to this project, including code style, commit messages, and Git workflow.

License

This project is licensed under the MIT License. See the LICENSE file for details.

Go Version

Go 1.25.3

Dependencies

  • github.com/nalgeon/be v0.3.0 for testing.