TanStack/table · warning

[table-worker] ${message}

Error message

[table-worker] ${message}

What it means

This is the generic dev-only warning channel for the worker-backed row-model feature. `warnOnce` prefixes every message with '[table-worker]' and fires at most once per stage. The two concrete messages are (1) the worker returned no result for a stage because the worker-side `initTableWorker()` config is missing that stage's RowModel factory, and (2) a non-contiguous offload: an upstream stage (e.g. 'filtered') is on the main thread while a downstream stage (e.g. 'sorted') is offloaded, so the upstream stage is bypassed and its computation is skipped in the worker.

Source

Thrown at packages/table-core/src/worker/createWorkerRowModel.ts:65

 */
export function createWorkerRowModel(
  tableWorker: TableWorker,
  stage: TableWorkerStage,
) {
  return <TFeatures extends TableFeatures, TData extends RowData>(
    _table: Table<TFeatures, TData>,
  ): (() => RowModel<TFeatures, TData>) => {
    const table = _table as unknown as AnyTable
    // Stages register per table as their factories initialize (first read).
    // The first render may therefore fire one extra coalesced request while
    // the stage set grows; single-flight makes it a cheap trailing request.
    getTableWorkerBridge(tableWorker, table).stages.add(stage)
    let warned = false

    const warnOnce = (message: string) => {
      if (process.env.NODE_ENV === 'development' && !warned) {
        warned = true
        console.warn(`[table-worker] ${message}`)
      }
    }

    const memoized = tableMemo({
      table,
      fnName: `table.get${capitalize(stage)}RowModel`,
      // The per-stage version bumps only when this stage's payload actually
      // changed, so stages the worker reported as `unchanged` skip the O(n)
      // rebuild entirely (the re-render itself rides the state slice bump).
      memoDeps: () => [
        table.getCoreRowModel(),
        getTableWorkerBridge(tableWorker, table).stageVersions[stage],
      ],
      fn: () => {
        const bridge = getTableWorkerBridge(tableWorker, table)
        const payload = bridge.results[stage]
        if (!payload) {
          if (bridge.resultRequestId > 0) {

View on GitHub (pinned to d01c01bedb)

Solutions

  1. Register the missing factory in the worker's `initTableWorker()` config (e.g. add sortedRowModel/filteredRowModel for every offloaded stage).
  2. Offload the full contiguous prefix: for every upstream stage in filtered -> grouped -> sorted -> expanded that the table uses, also call `createWorkerRowModel(tableWorker, '<upstream>')`.
  3. Rebuild/deploy the worker bundle so it matches the main-thread stage configuration.
  4. Ensure the worker file actually initializes the bridge (initTableWorker) before the table reads row models.

Example fix

// before (main thread)
features: tableFeatures({
  workerRowModelsFeature,
  sortedRowModel: createWorkerRowModel(tableWorker, 'sorted'),
})
// worker entry: initTableWorker({ createWorker }) // no factories -> no result

// after (worker entry)
initTableWorker({
  createWorker,
  filteredRowModel: createWorkerFilteredRowModel(),
  groupedRowModel: createWorkerGroupedRowModel(),
  sortedRowModel: createWorkerSortedRowModel(),
})
// main thread offloads the same contiguous prefix:
// filteredRowModel: createWorkerRowModel(tableWorker, 'filtered'),
// groupedRowModel: createWorkerRowModel(tableWorker, 'grouped'),
// sortedRowModel: createWorkerRowModel(tableWorker, 'sorted'),
Defensive patterns

Strategy: validation

Validate before calling

// Main thread: assert every offloaded stage is registered in the worker config
const offloaded = ['filtered', 'grouped', 'sorted'] // stages passed to createWorkerRowModel
const workerStages = ['filtered', 'grouped', 'sorted'] // stages configured in initTableWorker()
const missing = offloaded.filter((s) => !workerStages.includes(s))
if (missing.length) throw new Error(`Worker missing factories for: ${missing.join(', ')}`)
// Contiguity: offloaded stages must be a prefix of the pipeline
const pipeline = ['filtered', 'grouped', 'sorted', 'expanded']
const prefix = pipeline.slice(0, offloaded.length)
if (prefix.some((s) => !offloaded.includes(s))) {
  throw new Error('Offloaded stages must form a contiguous prefix of the pipeline')
}

Type guard

function isContiguousWorkerOffload(stages) {
  const pipeline = ['filtered', 'grouped', 'sorted', 'expanded']
  const idx = stages.map((s) => pipeline.indexOf(s)).sort((a, b) => a - b)
  return idx.every((v, i) => v === i)
}

Try / catch

// Worker round trips are async; a failed/absent result silently degrades to the
// pre-stage model, so guard reads if you must have worker output:
try {
  const result = await tableWorker.request({ type: 'run', requestId })
  if (!result?.results?.sorted) {
    throw new Error(`[table-worker] no 'sorted' result — check initTableWorker() config`)
  }
} catch (err) {
  console.error('[table-worker] stage failed, main thread fallback in effect:', err)
}

Prevention

When it happens

Trigger: Using `createWorkerRowModel(tableWorker, 'sorted')` (or 'grouped'/'expanded') while the worker bundle's `initTableWorker()` does not register the matching factory; or offloading a downstream stage without offloading the required contiguous upstream prefix (e.g. worker 'sorted' but main-thread 'filtered'/'grouped'). The warning only fires after a real round trip (resultRequestId > 0) and only once per stage.

Common situations: Registering `createWorkerRowModel` in the app but forgetting to add the same stage to the worker entry's `initTableWorker()` config; partial offloads that violate the contiguous-prefix rule; stale worker bundle predating newly added stages; results appearing stale because the stage silently returns its pre-stage model.

Related errors


AI-assisted analysis of TanStack/table@d01c01bedb (2026-08-28). Data as JSON: /api/errors/368cd256170c0346. Report an issue: GitHub.