conker: concurrency with the string attached

conker is a structured concurrency library for Go 1.25+, built on the lessons of sourcegraph/conc with a modernised stdlib-first approach.
go get github.com/purpleclay/conker
At a glance
- Use
pool.Pool if you want bounded, panic-safe concurrent task execution with recursive submission and error aggregation.
- Use
pool.ResultPool[T] if tasks produce typed results and you want them back in submission order.
- Use
conker.WaitGroup if you want a panic-safe replacement for sync.WaitGroup.
- Use
panics.Catcher if you want to catch panics in goroutines you manage yourself.
- Use
panics.ErrPanic with errors.Is to detect recovered panics without a type assertion.
Goals
Typed panics, not bare any
Go's recover() returns any. Every library that catches goroutine panics has to decide what to do with that opaque value, and the stdlib gives you nothing beyond the raw interface.
conker makes recovered panics first-class errors. *panics.Recovered implements error and integrates with errors.Is and errors.As — the same tools you already use for every other error in your program.
stdlib |
conker |
done := make(chan any, 1)
go func() {
defer func() {
done <- recover()
}()
doSomethingThatMightPanic()
}()
val := <-done
if val != nil {
// val is any — type-switch required,
// no errors.Is/As, stack lost
panic(val)
}
|
var c panics.Catcher
c.Try(doSomethingThatMightPanic)
if r := c.Recovered(); r != nil {
// r is *panics.Recovered — a typed error
errors.Is(r, panics.ErrPanic) // true
errors.Is(r, ErrTimeout) // true if panic value wrapped it
log.Print(r.Stack) // stack at panic site, not here
}
|
Panic-safe goroutines without boilerplate
Spawning goroutines safely with sync.WaitGroup requires a lot of ceremony: Add, Done, a deferred recover, and a decision about what to do with the panic value. conker.WaitGroup collapses all of that to one line.
Under the hood it delegates directly to sync.WaitGroup.Go — introduced in Go 1.25 — so there is no reimplementation of what the stdlib already does.
stdlib |
conker |
var wg sync.WaitGroup
for i := 0; i < 10; i++ {
wg.Add(1)
go func() {
defer wg.Done()
defer func() {
if v := recover(); v != nil {
// swallowed, or re-panic loses stack
}
}()
doSomething()
}()
}
wg.Wait()
|
var wg conker.WaitGroup
for i := 0; i < 10; i++ {
wg.Go(doSomething)
}
// re-panics as *panics.Recovered with
// the original stack trace preserved
wg.Wait()
|
When you want to handle a panic rather than propagate it, use WaitAndRecover:
var wg conker.WaitGroup
wg.Go(riskyOperation)
if r := wg.WaitAndRecover(); r != nil {
log.Printf("goroutine panicked: %v\n%s", r.Value, r.Stack)
}
Bounded concurrent tasks, not cancel-on-first-error
Running bounded concurrent tasks in Go typically means errgroup — and when used with errgroup.WithContext, it cancels remaining work on first error and still returns only one error from Wait.
pool.Pool collects every error from every task via errors.Join, recovers panics as typed *panics.Recovered errors, and passes each task its own context for per-task timeouts and backpressure via GoCtx.
errgroup |
conker |
g, ctx := errgroup.WithContext(context.Background())
g.SetLimit(10)
for _, item := range items {
g.Go(func() error {
return process(ctx, item)
})
}
// cancels remaining tasks on first error;
// Wait returns only that one error;
// panics crash the process
if err := g.Wait(); err != nil { ... }
|
p := pool.New().WithMaxGoroutines(10)
for _, item := range items {
p.Go(func(ctx context.Context) error {
return process(ctx, item)
})
}
// collects all errors via errors.Join;
// panics surface as *panics.Recovered
if err := p.Wait(); err != nil { ... }
|
All results collected, not silently dropped
sourcegraph/conc's error-variant pools (ResultErrorPool, ResultContextPool) drop results from failed tasks by default. The WithCollectErrored() option exists specifically to opt back in — meaning silent data loss is the default behaviour.
pool.ResultPool[T] always returns every result alongside every error. Nothing is silently discarded because a task failed or panicked.
sourcegraph/conc |
conker |
p := pool.NewWithResults[string]().WithErrors()
for _, id := range ids {
id := id
p.Go(func() (string, error) {
return fetch(id)
})
}
results, err := p.Wait()
// results from failed tasks are silently
// dropped unless WithCollectErrored() is set
|
p := pool.NewWithResults[string]()
for _, id := range ids {
p.Go(func(_ context.Context) (string, error) {
return fetch(id)
})
}
results, err := p.Wait()
// all results returned alongside errors;
// nothing is silently dropped
|
Roadmap
conker is pre-1.0. The following milestones track progress toward a stable API:
| Milestone |
Theme |
Status |
| v0.1.0 |
Foundations |
✅ |
| v0.2.0 |
Pool |
✅ |
| v0.3.0 |
Stream |
Planned |
| v0.4.0 |
Iter |
Planned |
| v0.5.0 |
Daemons & tooling |
Planned |
Upcoming highlights: stream.Stream with ordered concurrent callbacks, and native Go 1.23 iter.Seq/iter.Seq2 support in the iter package.
Examples
Each example is a self-contained, runnable program. External clients are stubbed so there are no runtime dependencies beyond the Go standard library and conker itself.
| Example |
Feature |
Use case |
etl-pipeline |
pool.ResultPool[T] |
ETL pipeline over an object store — download, transform, upload — with bounded concurrency, per-task timeouts, and submission-order results. |