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

  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.