fyne-io/fyne · error

async: misuse of unbounded channel, In() was closed

Error message

async: misuse of unbounded channel, In() was closed

What it means

UnboundedChan is fyne's internal unbounded queue (it backs the glfw event loop and lifecycle queues). Its contract is: shut down with Close(), never close(In()). The processing goroutine reads from ch.in in its outer select; a closed input yields ok==false, which the design treats as API misuse and converts into a deliberate panic so the bug is loud instead of silently dequeuing zero values forever.

Source

Thrown at internal/async/chan.go:52

// Close closes the channel.
func (ch *UnboundedChan[T]) Close() { ch.close <- struct{}{} }

func (ch *UnboundedChan[T]) processing() {
	// This is a preallocation of the internal unbounded buffer.
	// The size is randomly picked. But if one changes the size, the
	// reallocation size at the subsequent for loop should also be
	// changed too. Furthermore, there is no memory leak since the
	// queue is garbage collected.
	ch.q = make([]T, 0, 1<<10)
	for {
		select {
		case e, ok := <-ch.in:
			if !ok {
				// We don't want the input channel be accidentally closed
				// via close() instead of Close(). If that happens, it is
				// a misuse, do a panic as warning.
				panic("async: misuse of unbounded channel, In() was closed")
			}
			ch.q = append(ch.q, e)
		case <-ch.close:
			ch.closed()
			return
		}
		for len(ch.q) > 0 {
			select {
			case ch.out <- ch.q[0]:
				ch.q[0] = *new(T) // de-reference earlier to help GC (use clear() when Go 1.21 is base)
				ch.q = ch.q[1:]
			case e, ok := <-ch.in:
				if !ok {
					// We don't want the input channel be accidentally closed
					// via close() instead of Close(). If that happens, it is
					// a misuse, do a panic as warning.
					panic("async: misuse of unbounded channel, In() was closed")
				}

View on GitHub (pinned to 8860ee95c3)

Solutions

  1. Replace close(ch.In()) with ch.Close()
  2. With multiple producers sharing In(), track them yourself and call Close() exactly once after the last finishes - Close() is the single ownership point
  3. Grep the codebase for close( applied to channels obtained from In()

Example fix

// before
ch := async.NewUnboundedChan[int]()
... produce ...
close(ch.In()) // misuse: panics in the processing goroutine

// after
ch := async.NewUnboundedChan[int]()
... produce ...
ch.Close()
Defensive patterns

Strategy: validation

Validate before calling

// Containment: hand producers a send-only facade so they cannot close the channel.
type sender[T any] struct{ in chan<- T }

func (s sender[T]) Send(v T) { s.in <- v } // no Close exposed; owner calls ch.Close()

Prevention

When it happens

Trigger: Calling close() on the channel returned by In() - or on any alias stored as a chan T - anywhere in the program. The panic fires on the processing goroutine and crashes the whole process.

Common situations: Producer code that idiomatically closes its output channel when done (correct for plain channels); refactors that handed the send side to a helper which closes it; copying bounded-pipeline patterns onto an unbounded channel.

Related errors


AI-assisted analysis of fyne-io/fyne@8860ee95c3 (2026-08-15). Data as JSON: /api/errors/1046c946ddce1b4f. Report an issue: GitHub.