Fix 'fatal error: all goroutines are asleep - deadlock!' in Go

intermediate๐Ÿ”ท Go2026-03-21| Go 1.16+, all operating systems (Linux, macOS, Windows)

Error Message

fatal error: all goroutines are asleep - deadlock!
#goroutine#deadlock#concurrency#channel#sync

The Error

Your program crashes with:

fatal error: all goroutines are asleep - deadlock!

goroutine 1 [chan receive]:
main.main()
        /home/user/project/main.go:12 +0x58
exit status 2

Go's runtime detected that every goroutine is blocked โ€” nothing can move forward. So it kills the program instead of hanging forever. Think of it as Go doing you a favor.

Why This Happens

A deadlock occurs when all goroutines are stuck waiting on something that will never arrive. A channel with no counterpart. A mutex locked by the same goroutine trying to lock it again. A WaitGroup whose counter will never hit zero.

The usual suspects:

  • Sending to or receiving from a nil channel
  • Waiting on a channel that nobody writes to (or vice versa)
  • Forgetting wg.Done() inside a goroutine
  • Locking a non-reentrant mutex twice from the same goroutine
  • Unbuffered channel where sender and receiver are both in the same goroutine

Step-by-Step Fixes

1. Send with no receiver (or receive with no sender)

This one catches everyone eventually. You write to a channel but nobody's reading from it โ€” or the channel is nil:

// BAD: no goroutine is reading from ch
ch := make(chan int)
ch <- 42 // blocks forever โ†’ deadlock
fmt.Println(<-ch)

The fix: sender and receiver must run in separate goroutines.

ch := make(chan int)
go func() {
    ch <- 42
}()
fmt.Println(<-ch) // unblocks once the goroutine sends

If the receiver comes later and you just need to buffer one value, a buffered channel works too:

ch := make(chan int, 1) // capacity of 1
ch <- 42               // returns immediately
fmt.Println(<-ch)

2. Forgetting to close a channel (range loop hangs)

// BAD
ch := make(chan int)
go func() {
    for i := 0; i < 5; i++ {
        ch <- i
    }
    // forgot: close(ch)
}()
for v := range ch { // receives 0โ€“4, then waits forever
    fmt.Println(v)
}

A range over a channel keeps going until the channel is closed. No close(ch) โ€” the loop never ends.

go func() {
    for i := 0; i < 5; i++ {
        ch <- i
    }
    close(ch) // tells range: we're done
}()

3. WaitGroup counter never reaches zero

// BAD: if processItem panics or returns early, Done never fires
var wg sync.WaitGroup
for i := 0; i < 3; i++ {
    wg.Add(1)
    go func(n int) {
        processItem(n)
        // forgot wg.Done()
    }(i)
}
wg.Wait() // deadlock

Use defer wg.Done() as the very first line inside the goroutine. It runs even if the function panics:

go func(n int) {
    defer wg.Done() // guaranteed to fire
    processItem(n)
}(i)

4. Locking the same mutex twice in one goroutine

sync.Mutex is not reentrant. Lock it twice from the same goroutine and you'll hang:

// BAD
var mu sync.Mutex

func doWork() {
    mu.Lock()
    defer mu.Unlock()
    doMoreWork() // this also calls mu.Lock() โ†’ deadlock
}

func doMoreWork() {
    mu.Lock() // same goroutine already holds the lock
    defer mu.Unlock()
}

Restructure so the inner function assumes the lock is already held โ€” don't let it call Lock() itself:

func doWork() {
    mu.Lock()
    defer mu.Unlock()
    doMoreWorkLocked() // convention: "Locked" suffix = caller owns the lock
}

func doMoreWorkLocked() {
    // no mu.Lock() here
}

5. Nil channel

Sending to or receiving from a nil channel blocks forever โ€” no panic, just silence:

// BAD
var ch chan int // nil
ch <- 1        // blocks forever

Always initialize with make:

ch := make(chan int)     // unbuffered
ch := make(chan int, 10) // buffered, capacity 10

Reading the Goroutine Stack Dump

When the deadlock fires, Go prints a full goroutine dump. Don't skip it โ€” it tells you exactly where each goroutine is stuck:

goroutine 1 [chan receive]:
main.main()
        /home/user/project/main.go:12 +0x58

[chan receive] โ€” blocked waiting to receive from a channel. [semacquire] โ€” blocked on a mutex lock. [sleep] โ€” inside time.Sleep (won't deadlock on its own).

Match the state tag and line number to your code โ€” that's your deadlock site.

Also worth running with the race detector, since data races and deadlocks often show up together:

go run -race main.go
go test -race ./...

Verify the Fix

Clean exit means the deadlock is gone:

# Should exit with code 0, no panic output
go run main.go

# Tests + race detection
go test -race ./...

# For long-running programs, check for goroutine leaks via pprof
import _ "net/http/pprof"
// curl http://localhost:6060/debug/pprof/goroutine?debug=1

No fatal error in the output and exit code 0 โ€” you're good.

Quick Checklist

  • Every channel send has a receiver running in a different goroutine
  • Channels used with range are close()d by the producer
  • Every wg.Add(1) is paired with a defer wg.Done()
  • No goroutine locks the same sync.Mutex twice
  • No channel variable is nil at send/receive time
  • No select without a default case blocks all goroutines at once

Tips

  • Go's deadlock detector only fires when all goroutines are blocked. If even one goroutine is still running (say, a spinner loop), the runtime won't catch it. Use pprof to spot goroutine leaks in that case.
  • For cancellation logic, reach for context.Context with a timeout rather than rolling your own channel protocol. It's cleaner and prevents goroutines from leaking silently.
  • Add goleak (github.com/uber-go/goleak) to your test suite. It fails the test if any goroutine is still running when the test exits โ€” catches leaks before they hit production.

Related Error Notes