chenglou/pretext · critical · Error
Text measurement requires OffscreenCanvas or a DOM canvas co
Error message
Text measurement requires OffscreenCanvas or a DOM canvas context.
What it means
The only published-library error in this set. getMeasureContext lazily builds the 2D canvas context that prepare()/prepareWithSegments() use to measure segment widths (via getSegmentMetrics and other internal callers at measurement.ts:63,135,260). It tries OffscreenCanvas first, then document.createElement('canvas').getContext('2d'), and throws if neither global exists. So the library cannot measure text in an environment with no canvas at all.
Source
Thrown at src/measurement.ts:48
const emojiPresentationRe = /\p{Emoji_Presentation}/u
const maybeEmojiRe = /[\p{Emoji_Presentation}\p{Extended_Pictographic}\p{Regional_Indicator}\uFE0F\u20E3]/u
let sharedGraphemeSegmenter: Intl.Segmenter | null = null
const emojiCorrectionCache = new Map<string, number>()
export function getMeasureContext(): CanvasRenderingContext2D | OffscreenCanvasRenderingContext2D {
if (measureContext !== null) return measureContext
if (typeof OffscreenCanvas !== 'undefined') {
measureContext = new OffscreenCanvas(1, 1).getContext('2d')!
return measureContext
}
if (typeof document !== 'undefined') {
measureContext = document.createElement('canvas').getContext('2d')!
return measureContext
}
throw new Error('Text measurement requires OffscreenCanvas or a DOM canvas context.')
}
export function getSegmentMetricCache(font: string): Map<string, SegmentMetrics> {
let cache = segmentMetricCaches.get(font)
if (!cache) {
cache = new Map()
segmentMetricCaches.set(font, cache)
}
return cache
}
export function getSegmentMetrics(seg: string, cache: Map<string, SegmentMetrics>): SegmentMetrics {
let metrics = cache.get(seg)
if (metrics === undefined) {
const ctx = getMeasureContext()
metrics = {
width: ctx.measureText(seg).width,
containsCJK: isCJK(seg),View on GitHub (pinned to ac49b09b7d)
Solutions
- Polyfill OffscreenCanvas before calling prepare(): install node-canvas (npm i canvas) or @napi-rs/canvas, then assign globalThis.OffscreenCanvas to a class whose getContext('2d') returns the node-canvas 2D context.
- Run the measurement in a browser or web worker that has OffscreenCanvas, instead of Node.
- Guard the call site: only invoke prepare() when typeof OffscreenCanvas !== 'undefined' || typeof document !== 'undefined'.
- For SSR, defer prepare() to the client, or precompute prepared handles in a build step that has canvas, then ship them to layout() (which is pure arithmetic).
Example fix
// before — runs in Node, throws
import { prepare, layout } from '@chenglou/pretext'
const p = prepare('hello world', '16px Inter')
// after — provide an OffscreenCanvas via node-canvas before importing the library
import { createCanvas } from 'canvas'
class OffscreenCanvasPolyfill {
#ctx
constructor(w = 1, h = 1) { this.#ctx = createCanvas(w, h).getContext('2d') }
getContext() { return this.#ctx }
}
;(globalThis as any).OffscreenCanvas = OffscreenCanvasPolyfill
import { prepare, layout } from '@chenglou/pretext'
const p = prepare('hello world', '16px Inter') Defensive patterns
Strategy: validation
Validate before calling
const canMeasure =
typeof OffscreenCanvas !== 'undefined' ||
(typeof document !== 'undefined' && !!document.createElement('canvas').getContext('2d'))
if (!canMeasure) {
throw new Error('Pretext prepare() needs OffscreenCanvas or a DOM canvas in this environment; install node-canvas or run in a browser/worker.')
} Type guard
const hasCanvas = (): boolean => typeof OffscreenCanvas !== 'undefined' || typeof document !== 'undefined'
Try / catch
try {
return prepare(text, font, opts)
} catch (e) {
if (e instanceof Error && /Text measurement requires/.test(e.message)) {
// Either install an OffscreenCanvas polyfill (node-canvas / @napi-rs/canvas)
// or skip prepare() on the server and only use layout() over precomputed handles.
}
throw e
} Prevention
- Install a canvas polyfill (node-canvas or @napi-rs/canvas) and assign globalThis.OffscreenCanvas before any prepare() call in Node.
- Never call prepare() at module-eval time; defer it to a runtime path that has canvas.
- Gate SSR imports so prepare() only runs client-side, or precompute prepared handles in a build step that has canvas.
- Prefer OffscreenCanvas-capable runtimes (modern browsers, web workers, Chromium) for server-side measurement.
When it happens
Trigger: Calling prepare() or prepareWithSegments() in plain Node/Bun (no DOM, no OffscreenCanvas); a worker/runtime (older workerd, jsdom without the canvas package) where OffscreenCanvas is undefined and document is absent or lacks canvas; SSR that imports the library and calls prepare during render.
Common situations: Server-side height precomputation; tests in jsdom (jsdom's canvas returns null for getContext('2d') unless the canvas package is installed); bundlers that guard globals; React/Next SSR importing the module and calling prepare at module-eval time.
Related errors
- Failed to measure ${currentMeta?.id ?? 'corpus'} @ ${width}
- Benchmark run ${index + 1} failed: ${report.message ?? 'unkn
- Unsupported browser ${browser}; expected chrome or safari
- Unsupported browser ${browser}; expected chrome or safari
AI-assisted analysis of chenglou/pretext@ac49b09b7d (2026-08-12).
Data as JSON: /api/errors/5c3c469b085962e6.
Report an issue: GitHub.