thanos-io/thanos · error
channel closed before a value received
Error message
channel closed before a value received
What it means
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.
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
Defensive patterns
Strategy: try-catch
Try / catch
if err := ss.Err(); err != nil {
if strings.Contains(err.Error(), "channel closed before a value received") {
// internal select-goroutine failure; surface as 500 with logs
return fmt.Errorf("query engine failed internally: %w", err)
}
return err
} Prevention
- 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
When it happens
Trigger: 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.
Common situations: Seen during Thanos engine bugs, panics inside the select goroutine, or when the querier is torn down mid-select; not a user-facing misconfiguration.
Understand the failure class
Background: "This is a bug, please report it": internal invariant violations, unreachable panics, and SNH errors explained — this error's family across 47 libraries.
Related errors
- unknown partial response strategy
- add thanos opts query params
- unexpected result aggregate type
- no valid chunk found
- got empty chunks
AI-assisted analysis of thanos-io/thanos@35b8b99117 (2026-09-07).
Data as JSON: /api/errors/48f99765367e37d9.
Report an issue: GitHub.
Appendix: source
Thrown at pkg/query/querier.go:351
set, stats, err := q.selectFn(ctx, hints, ms...)
if err != nil {
promise <- storage.ErrSeriesSet(err)
return
}
q.seriesStatsReporter(stats)
promise <- set
}()
return &lazySeriesSet{create: func() (storage.SeriesSet, bool) {
defer cancel()
defer span.Finish()
// Only gets called once, for the first Next() call of the series set.
set, ok := <-promise
if !ok {
return storage.ErrSeriesSet(errors.New("channel closed before a value received")), false
}
return set, set.Next()
}}
}
const SeriesHashLabelName = "__cf_series_hash__"
func (q *querier) selectFn(ctx context.Context, hints *storage.SelectHints, ms ...*labels.Matcher) (storage.SeriesSet, storepb.SeriesStatsCounter, error) {
sms, err := storepb.PromMatchersToMatchers(ms...)
if err != nil {
return nil, storepb.SeriesStatsCounter{}, errors.Wrap(err, "convert matchers")
}
aggrs := aggrsFromFunc(hints.Func)
maxResolutionMillis := maxResolutionFromSelectHints(q.maxResolutionMillis, hints.Range, hints.Func)
// TODO(bwplotka): Pass it using the SeriesRequest instead of relying on context.
ctx = context.WithValue(ctx, store.StoreMatcherKey, q.storeDebugMatchers)View on GitHub (pinned to 35b8b99117)