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.

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.

Appendix: 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)