zed-industries/zed · error

Canvas readback option could not be set

Error message

Canvas readback option could not be set

What it means

Raised in `TextCanvas::new` when the `willReadFrequently` option could not be assigned on the canvas context options object via `js_sys::Reflect::set` — `Reflect::set` succeeded (no exception) but returned `false`, meaning the property could not be created/written. Without this option, subsequent `getImageData` readbacks would be slower, so the library treats a silent assignment failure as fatal. On wasm targets this usually indicates the options object is frozen, non-extensible, or has a non-writable 'willReadFrequently' property.

Solutions

  1. Run in a standard browser environment where OffscreenCanvas context options are supported
  2. Ensure the options object is a plain mutable `js_sys::Object` — don't pass frozen or sealed objects
  3. Check for polyfills/proxies overriding Object or Reflect behavior in the page
  4. Update the browser/wasm runtime to a version with full OffscreenCanvas 2D support

Example fix

// before
let assigned = js_sys::Reflect::set(&options, &JsValue::from_str("willReadFrequently"), &JsValue::TRUE)
    .map_err(|error| anyhow!("setting Canvas readback option: {error:?}"))?;
ensure!(assigned, "Canvas readback option could not be set");
// after
let options = js_sys::Object::new(); // fresh, extensible object
js_sys::Reflect::set(&options, &JsValue::from_str("willReadFrequently"), &JsValue::TRUE)
    .map_err(|error| anyhow!("setting Canvas readback option: {error:?}"))?
    .then_some(())
    .ok_or_else(|| anyhow!("Canvas readback option could not be set"))?;
Defensive patterns

Strategy: try-catch

Validate before calling

// guard before constructing
let options = js_sys::Object::new();
let assigned = js_sys::Reflect::set(&options, &JsValue::from_str("willReadFrequently"), &JsValue::TRUE)
    .unwrap_or(false);
if !assigned {
    return Err(anyhow!("environment rejects canvas context options"));
}

Type guard

fn is_plain_object(value: &JsValue) -> bool {
    js_sys::Object::is_extensible(value) && !js_sys::Object::is_frozen(value) && !js_sys::Object::is_sealed(value)
}

Try / catch

match TextCanvas::new() {
    Ok(canvas) => canvas,
    Err(e) if e.to_string().contains("readback option") => {
        // degraded environment: fall back to non-offscreen canvas or log and disable text caching
        log::warn!("canvas options unsupported: {e}");
        fallback_renderer()
    }
    Err(e) => return Err(e),
}

Prevention

When it happens

Trigger: Calling the canvas-text constructor in gpui_web with an options object created with `Object::freeze`, or an environment/proxy (e.g. a strict security policy or monkey-patched Object) where the 'willReadFrequently' property is defined as non-writable, making `Reflect::set` return false.

Common situations: Custom wasm shim/proxy objects injected by test harnesses or browser extensions, hardened JS environments with frozen globals, or exotic embedding runtimes (non-browser JS engines) that don't fully support OffscreenCanvas option objects.

Understand the failure class

Background: "This is a bug, please report it": internal invariant violations, unreachable panics, and SNH errors explained — this error's family across 47 libraries.

Related errors


AI-assisted analysis of zed-industries/zed@916fc2b8cb (2026-09-19). Data as JSON: /api/errors/bbf22c8586277aec. Report an issue: GitHub.

Appendix: source

Thrown at crates/gpui_web/src/canvas_text.rs:40

struct TextCanvas {
    canvas: OffscreenCanvas,
    context: OffscreenCanvasRenderingContext2d,
}

impl TextCanvas {
    fn new() -> Result<Self> {
        let canvas = OffscreenCanvas::new(1, 1)
            .map_err(|error| anyhow!("creating text OffscreenCanvas: {error:?}"))?;
        let options = js_sys::Object::new();
        // Every rasterized glyph is read back for upload into GPUI's atlas.
        let assigned = js_sys::Reflect::set(
            &options,
            &JsValue::from_str("willReadFrequently"),
            &JsValue::TRUE,
        )
        .map_err(|error| anyhow!("setting Canvas readback option: {error:?}"))?;
        ensure!(assigned, "Canvas readback option could not be set");
        let context = canvas
            .get_context_with_context_options("2d", &options)
            .map_err(|error| anyhow!("getting OffscreenCanvas 2D context: {error:?}"))?
            .context("OffscreenCanvas 2D text rendering is unavailable")?
            .dyn_into::<OffscreenCanvasRenderingContext2d>()
            .map_err(|error| anyhow!("unexpected OffscreenCanvas 2D context: {error:?}"))?;
        Ok(Self { canvas, context })
    }

    fn configure(&self, css_font: &str) -> Result<()> {
        ensure!(!css_font.trim().is_empty(), "Canvas text font is empty");
        // An invalid CSS font assignment is ignored by Canvas. Reset first so
        // it cannot accidentally inherit the preceding request's font.
        self.context.set_font("10px sans-serif");
        self.context.set_font(css_font);
        self.context.set_text_align("left");
        self.context.set_text_baseline("alphabetic");
        self.context.set_fill_style_str("white");

View on GitHub (pinned to 916fc2b8cb)