{"record":{"id":"1046c946ddce1b4f","repo":"fyne-io/fyne","slug":"async-misuse-of-unbounded-channel-in-was-close","errorCode":null,"errorMessage":"async: misuse of unbounded channel, In() was closed","messagePattern":"async: misuse of unbounded channel, In\\(\\) was closed","errorType":"panic","errorClass":null,"httpStatus":null,"severity":"error","filePath":"internal/async/chan.go","lineNumber":52,"sourceCode":"\n// Close closes the channel.\nfunc (ch *UnboundedChan[T]) Close() { ch.close <- struct{}{} }\n\nfunc (ch *UnboundedChan[T]) processing() {\n\t// This is a preallocation of the internal unbounded buffer.\n\t// The size is randomly picked. But if one changes the size, the\n\t// reallocation size at the subsequent for loop should also be\n\t// changed too. Furthermore, there is no memory leak since the\n\t// queue is garbage collected.\n\tch.q = make([]T, 0, 1<<10)\n\tfor {\n\t\tselect {\n\t\tcase e, ok := <-ch.in:\n\t\t\tif !ok {\n\t\t\t\t// We don't want the input channel be accidentally closed\n\t\t\t\t// via close() instead of Close(). If that happens, it is\n\t\t\t\t// a misuse, do a panic as warning.\n\t\t\t\tpanic(\"async: misuse of unbounded channel, In() was closed\")\n\t\t\t}\n\t\t\tch.q = append(ch.q, e)\n\t\tcase <-ch.close:\n\t\t\tch.closed()\n\t\t\treturn\n\t\t}\n\t\tfor len(ch.q) > 0 {\n\t\t\tselect {\n\t\t\tcase ch.out <- ch.q[0]:\n\t\t\t\tch.q[0] = *new(T) // de-reference earlier to help GC (use clear() when Go 1.21 is base)\n\t\t\t\tch.q = ch.q[1:]\n\t\t\tcase e, ok := <-ch.in:\n\t\t\t\tif !ok {\n\t\t\t\t\t// We don't want the input channel be accidentally closed\n\t\t\t\t\t// via close() instead of Close(). If that happens, it is\n\t\t\t\t\t// a misuse, do a panic as warning.\n\t\t\t\t\tpanic(\"async: misuse of unbounded channel, In() was closed\")\n\t\t\t\t}","sourceCodeStart":34,"sourceCodeEnd":70,"githubUrl":"https://github.com/fyne-io/fyne/blob/8860ee95c356385effa5a7be51adb11c6be9d2b6/internal/async/chan.go#L34-L70","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","solutions":["Replace close(ch.In()) with ch.Close()","With multiple producers sharing In(), track them yourself and call Close() exactly once after the last finishes - Close() is the single ownership point","Grep the codebase for close( applied to channels obtained from In()"],"exampleFix":"// before\nch := async.NewUnboundedChan[int]()\n... produce ...\nclose(ch.In()) // misuse: panics in the processing goroutine\n\n// after\nch := async.NewUnboundedChan[int]()\n... produce ...\nch.Close()","handlingStrategy":"validation","validationCode":"// Containment: hand producers a send-only facade so they cannot close the channel.\ntype sender[T any] struct{ in chan<- T }\n\nfunc (s sender[T]) Send(v T) { s.in <- v } // no Close exposed; owner calls ch.Close()","typeGuard":null,"tryCatchPattern":null,"preventionTips":["Treat In() as send-only forever; the single owner calls Close() once","Never store In() as a plain chan T where helper code might close it","Grep for 'close(' near any channel obtained from In() during review"],"tags":["go","concurrency","channels","api-misuse","async","internal"],"backgroundTag":null,"analyzedSha":"8860ee95c356385effa5a7be51adb11c6be9d2b6","analyzedAt":"2026-08-15T22:01:51.624Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}