tldraw/tldraw · error · Error

Tu must be finite and greater than Tc

Error message

Tu must be finite and greater than Tc

What it means

Thrown by the commenting clustering module when Tu (the uncluster threshold — the minimum temporal/spatial separation above which clusters split apart) is not finite or is not greater than Tc. Tu must exceed Tc because clustering contracts below Tc and splits above Tu; Tu <= Tc would collapse the two regimes into a contradiction.

Source

Thrown at packages/commenting/src/clustering/computeClusterTable.ts:63

		maxSplitZoom: opts.maxSplitZoom,
	})

	return { events, leaves: leafNodes }
}

function resolveOptions(options: ClusterOptions): ResolvedClusterOptions {
	const Tc = options.Tc ?? 22
	const Tu = options.Tu ?? 1.2 * Tc
	const eps = options.eps ?? 0.7
	const Dmax = options.Dmax ?? 3.75 * Tc
	const maxSplitZoom = options.maxSplitZoom ?? 6
	const { minZoom, maxZoom } = options

	if (!Number.isFinite(Tc) || Tc <= 0) {
		throw new Error('Tc must be finite and greater than 0')
	}
	if (!Number.isFinite(Tu) || Tu <= Tc) {
		throw new Error('Tu must be finite and greater than Tc')
	}
	if (!Number.isFinite(eps) || eps < 0) {
		throw new Error('eps must be finite and greater than or equal to 0')
	}
	if (!Number.isFinite(Dmax) || Dmax < Tc) {
		throw new Error('Dmax must be finite and greater than or equal to Tc')
	}
	if (!Number.isFinite(minZoom) || minZoom <= 0) {
		throw new Error('minZoom must be finite and greater than 0')
	}
	if (!Number.isFinite(maxZoom) || maxZoom <= minZoom) {
		throw new Error('maxZoom must be finite and greater than minZoom')
	}
	if (!Number.isFinite(maxSplitZoom) || maxSplitZoom <= 0) {
		throw new Error('maxSplitZoom must be finite and greater than 0')
	}

	return { Tc, Tu, eps, Dmax, minZoom, maxZoom, maxSplitZoom }

View on GitHub (pinned to b31086b447)

Solutions

  1. Ensure Tu > Tc; if customizing Tc, either omit Tu (so it defaults to 1.2*Tc) or compute Tu from Tc: `Tu = Tc * 1.2`.
  2. Validate the relationship before calling: `if (!(Tu > Tc)) throw ...` in your own config layer.
  3. Review config presets to confirm Tu/Tc ordering matches the clustering contract.
  4. Clamp: `const Tu = Math.max(options.Tu ?? 1.2 * Tc, Tc + 1)`.

Example fix

// before
const Tu = options.Tu ?? 1.2 * Tc
if (!Number.isFinite(Tu) || Tu <= Tc) {
  throw new Error('Tu must be finite and greater than Tc')
}

// caller side: derive Tu from Tc so the invariant always holds
const Tc = 40
computeClusterTable(leaves, { Tc, Tu: Tc * 1.2, minZoom, maxZoom })
Defensive patterns

Strategy: validation

Validate before calling

function assertValidTu(Tc: number, options: ClusterOptions): void {
  const Tu = options.Tu ?? 1.2 * Tc
  if (!Number.isFinite(Tu) || Tu <= Tc) {
    throw new Error(`options.Tu must be finite and > Tc (${Tc}). Got ${Tu}. Omit Tu to default to 1.2*Tc.`)
  }
}

Type guard

function isValidTu(Tu: unknown, Tc: number): boolean {
  return typeof Tu === 'number' && Number.isFinite(Tu) && Tu > Tc
}

Prevention

When it happens

Trigger: Passing options.Tu less than or equal to options.Tc, or leaving Tu unset while a custom Tc larger than the default Tu multiple (1.2*Tc) is provided (not possible by default, but a future change could). Explicitly passing Tu = NaN or Infinity. The most common case is Tu <= Tc after both are customized independently.

Common situations: A host tunes Tc up (e.g. to 40) and also sets Tu to a fixed value (e.g. 30) that no longer exceeds Tc. A config preset has Tu and Tc swapped. Tu is derived from a formula that can drop below Tc under certain canvas conditions.

Related errors


AI-assisted analysis of tldraw/tldraw@b31086b447 (2026-08-12). Data as JSON: /api/errors/63733ba1bc8051f8. Report an issue: GitHub.