{"record":{"id":"48f99765367e37d9","repo":"thanos-io/thanos","slug":"channel-closed-before-a-value-received","errorCode":null,"errorMessage":"channel closed before a value received","messagePattern":"channel closed before a value received","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"pkg/query/querier.go","lineNumber":351,"sourceCode":"\n\t\tset, stats, err := q.selectFn(ctx, hints, ms...)\n\t\tif err != nil {\n\t\t\tpromise <- storage.ErrSeriesSet(err)\n\t\t\treturn\n\t\t}\n\t\tq.seriesStatsReporter(stats)\n\n\t\tpromise <- set\n\t}()\n\n\treturn &lazySeriesSet{create: func() (storage.SeriesSet, bool) {\n\t\tdefer cancel()\n\t\tdefer span.Finish()\n\n\t\t// Only gets called once, for the first Next() call of the series set.\n\t\tset, ok := <-promise\n\t\tif !ok {\n\t\t\treturn storage.ErrSeriesSet(errors.New(\"channel closed before a value received\")), false\n\t\t}\n\t\treturn set, set.Next()\n\t}}\n}\n\nconst SeriesHashLabelName = \"__cf_series_hash__\"\n\nfunc (q *querier) selectFn(ctx context.Context, hints *storage.SelectHints, ms ...*labels.Matcher) (storage.SeriesSet, storepb.SeriesStatsCounter, error) {\n\tsms, err := storepb.PromMatchersToMatchers(ms...)\n\tif err != nil {\n\t\treturn nil, storepb.SeriesStatsCounter{}, errors.Wrap(err, \"convert matchers\")\n\t}\n\n\taggrs := aggrsFromFunc(hints.Func)\n\tmaxResolutionMillis := maxResolutionFromSelectHints(q.maxResolutionMillis, hints.Range, hints.Func)\n\n\t// TODO(bwplotka): Pass it using the SeriesRequest instead of relying on context.\n\tctx = context.WithValue(ctx, store.StoreMatcherKey, q.storeDebugMatchers)","sourceCodeStart":333,"sourceCodeEnd":369,"githubUrl":"https://github.com/thanos-io/thanos/blob/35b8b991177def87ed52dcf10f9b6d87f07282c8/pkg/query/querier.go#L333-L369","documentation":"querier.Select spawns a goroutine that computes the series set and sends it exactly once on the promise channel. Next() on the lazy series set receives from this channel; if it is closed before a value is delivered, the implementation returns this error — a safety net against a broken/garbage-collected select goroutine.","triggerScenarios":"The promise channel is closed without a value, e.g. the select goroutine panics/returns before sending, or internal lifecycle mishandling of the lazy series set.","commonSituations":"Seen during Thanos engine bugs, panics inside the select goroutine, or when the querier is torn down mid-select; not a user-facing misconfiguration.","solutions":["Look for a panic/error higher in the logs from the select goroutine","Check querier lifetime: ensure the query is not cancelled/torn down while Next is pending","Upgrade Thanos — fixed versions send an error series set instead of closing the channel","File an issue with stack trace if reproducible"],"exampleFix":null,"handlingStrategy":"try-catch","validationCode":null,"typeGuard":null,"tryCatchPattern":"if err := ss.Err(); err != nil {\n    if strings.Contains(err.Error(), \"channel closed before a value received\") {\n        // internal select-goroutine failure; surface as 500 with logs\n        return fmt.Errorf(\"query engine failed internally: %w\", err)\n    }\n    return err\n}","preventionTips":["Do not cancel querier mid-Select unless the caller truly aborts","Keep Thanos up to date (fixed lifecycle handling)","Alert on this error — it always indicates a bug"],"tags":["thanos","query","goroutine","channel"],"backgroundTag":"internal-invariant-violation","analyzedSha":"35b8b991177def87ed52dcf10f9b6d87f07282c8","analyzedAt":"2026-09-07T01:49:59.689Z","contentChangedAt":"2026-09-07T01:49:59.689Z","schemaVersion":2},"datasetVersion":"2026-09-15T23:17:13.987Z"}