Go WaitGroup panic — Add Called After Wait / Negative Counter
Fix sync.WaitGroup misuse panics by calling Add before starting goroutines, never inside them, and rejecting a negative counter.
Symptoms
- panic: sync: WaitGroup is reused before previous Wait has returned
- panic: sync: negative WaitGroup counter
- The race detector reports a WaitGroup misuse warning
Possible causes
- wg.Add is called inside the spawned goroutine instead of before it starts
- wg.Done is called more times than wg.Add
- The same WaitGroup is reused for a second batch before the first Wait returns
Diagnosis steps
Reproduce the misuse under the race detector
go test -race ./...Interpretation
The race detector flags Add-after-Wait and concurrent Add/Wait, naming the exact lines.
Audit Add placement
grep -rn "wg.Add\|wg.Done" .Interpretation
Every Add must execute before the goroutine it tracks is launched; Done belongs inside the goroutine.
Root cause
WaitGroup requires a positive Add before Wait and before the tracked goroutines start. Calling Add concurrently with Wait, or after the counter reaches zero, races the internal state and panics with reuse or negative-counter errors.
Fix
Call wg.Add(n) once, before launching the n goroutines, put defer wg.Done() at the top of each goroutine, and use a fresh WaitGroup per batch or errgroup instead of reusing one.
Risk note
Copying a WaitGroup by value is also a bug; always pass it by pointer or embed it in a struct.
Verify the fix
Rerun with -race and -count=100 and confirm no panic and no warnings; assert the number of completed jobs equals n.
Symptoms
This guide profiles the failure behind “Go WaitGroup panic — Add Called After Wait / Negative Counter”. It walks through the observable symptoms, a reproducible diagnosis path, the underlying root cause, the concrete fix, and how to verify the change and prevent a regression.
Observed symptoms include:
- panic: sync: WaitGroup is reused before previous Wait has returned
- panic: sync: negative WaitGroup counter
- The race detector reports a WaitGroup misuse warning
Reproduce and Diagnose
Reproduce the failure in isolation before changing any code. Work through the checks below in order; each one narrows the search space.
Step 1: Reproduce the misuse under the race detector
go test -race ./...
The race detector flags Add-after-Wait and concurrent Add/Wait, naming the exact lines.
Step 2: Audit Add placement
grep -rn "wg.Add\|wg.Done" .
Every Add must execute before the goroutine it tracks is launched; Done belongs inside the goroutine.
Likely contributors, in order of frequency:
- wg.Add is called inside the spawned goroutine instead of before it starts
- wg.Done is called more times than wg.Add
- The same WaitGroup is reused for a second batch before the first Wait returns
Root Cause
WaitGroup requires a positive Add before Wait and before the tracked goroutines start. Calling Add concurrently with Wait, or after the counter reaches zero, races the internal state and panics with reuse or negative-counter errors.
The Fix
Call wg.Add(n) once, before launching the n goroutines, put defer wg.Done() at the top of each goroutine, and use a fresh WaitGroup per batch or errgroup instead of reusing one.
Watch out: Copying a WaitGroup by value is also a bug; always pass it by pointer or embed it in a struct.
var wg sync.WaitGroup
wg.Add(len(urls)) // BEFORE launching
for _, u := range urls {
u := u
go func() {
defer wg.Done() // inside the goroutine
fetch(u)
}()
}
wg.Wait()
// then a fresh WaitGroup for the next batch — never reuse a waited one.
wg = sync.WaitGroup{}
Verify and Prevent
Rerun with -race and -count=100 and confirm no panic and no warnings; assert the number of completed jobs equals n.
Once verified, add a regression test that reproduces the original symptom so the failure cannot silently return.
This class of failure is usually environmental rather than pure logic, so capture the exact command, the environment variables, and the working directory in your bug report. Reproduce it in the smallest possible script, then widen the scope only once the minimal case is understood. A short written checklist for this failure mode saves hours the next time it appears.