chenglou/pretext · critical · Error

Text measurement requires OffscreenCanvas or a DOM canvas…

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.

Solutions

  1. 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.
  2. Run the measurement in a browser or web worker that has OffscreenCanvas, instead of Node.
  3. Guard the call site: only invoke prepare() when typeof OffscreenCanvas !== 'undefined' || typeof document !== 'undefined'.
  4. 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

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


AI-assisted analysis of chenglou/pretext@ac49b09b7d (2026-08-12). Data as JSON: /api/errors/5c3c469b085962e6. Report an issue: GitHub.

Appendix: 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)