Structured concurrency
helps reasoning about the behaviour of parallel programs. parallel implements
structured concurrency for Go.
func subtask(ctx context.Context) error {
// to be run in parallel
}
type subtaskWithData struct { /* ... */ }
func (swd *subtaskWithData) Run(ctx context.Context) error {
// to be run in parallel
}
err := parallel.Run(ctx, func(ctx context.Context, spawn parallel.SpawnFn) error {
swd := &subtaskWithData{}
// do some synchronous initialization here
spawn("subtask", parallel.Fail, subtask)
spawn("subtaskWithData", parallel.Fail, swd.Run)
return nil
})
Runs initialization within parallel.Run(), and then waits until the context is
canceled, or one of the spawned tasks finishes. Panics in goroutines are captured.
See the documentation for additional features:
- task groups without inversion of control
- tasks that may exit and keep the group running
- tasks that may exit and cause the group to stop gracefully
golang.org/x/sync/errgroup covers the common case: run N goroutines, cancel a
shared context when one fails, return that error. parallel differs where a
long-lived process needs more than that.
- Tasks have names, and the name appears in the error.
errgroup's tasks are anonymous, so a failure tells you what went wrong but not which of them went wrong. - Exiting is a policy, not an accident.
Continue,ExitandFailsay what a task returning nil means: a finite job that is done, a request to shut the group down gracefully, or a task that should never have stopped and whose stopping is itself a failure.errgrouphas no way to express the third. - Every failure survives.
GroupFailedError.Errors()lists all of them in order and names the one that caused the shutdown.errgroupkeeps the first error and discards the rest. - Cancellation carries a cause.
context.Causeon the group's context returns aClosingErrornaming why shutdown began, so a task can tell being interrupted apart from failing. - Panics become errors rather than unwinding the program.
- Initialization runs inside the group, so a setup failure shuts down whatever already started.
A task group has to answer three different questions when something goes wrong, and they have three different answers:
- What did the group return?
GroupFailedError. It unwraps as the cause, andErrors()lists every task failure that was not itself a consequence of the shutdown, in chronological order. - Why was the context canceled?
ClosingError, available throughcontext.Cause. It names the cause and the group, so cancellation of a parent group is distinguishable from cancellation of a child. - What did an individual task do?
TaskFailedError, orTaskSucceededErrorfor a task that finished cleanly in a mode where that ends the group.
ClosingError deliberately unwraps as context.Canceled rather than as its own
cause. If task A fails to read a file and the ensuing shutdown interrupts its
sibling task B, it does not mean that B failed to read a file — only that B was
interrupted. Unwrapping to the cause would make errors.Is(err, fs.ErrNotExist)
report true for a task that never touched the filesystem.
This library was written at Ridge and first published, at version 0.1.x, as dottedmag/parallel. It was later carried into a second codebase, where the error model above replaced the original single-error reporting: the earlier version kept only the first error, reported task failures as formatted strings, cancelled without a cause, and distinguished shutdown-induced cancellations from real ones by heuristic. Panic capture moved out into fault.
Copyright Tectonic Labs Ltd. (original), Onboard Inc. (error model, 2026).
Licensed under Apache 2.0 license.
Authors: