GraphiteEditor/Graphite · error

Context should be a canvas 2d context

Error message

Context should be a canvas 2d context

What it means

After get_context("2d") succeeds, dyn_into::<OffscreenCanvasRenderingContext2d>() performs an unchecked-at-compile-time JS type downcast and this .expect("Context should be a canvas 2d context") panics if the returned JsValue is not actually an OffscreenCanvasRenderingContext2d. That happens when the runtime returns a different object for "2d" (a plain CanvasRenderingContext2D from a polyfill, or an incomplete OffscreenCanvas implementation). In fully compliant browsers this is effectively unreachable.

Source

Thrown at editor/src/messages/portfolio/document/overlays/utility_types_web.rs:1028

		self.render_context.set_fill_style_str(color);
		self.render_context.fill();
	}

	/// Fills the area inside the path with a pattern. Assumes `color` is an sRGB hex string.
	/// Used by the fill tool to show the area to be filled.
	pub fn fill_path_pattern(&mut self, subpaths: impl Iterator<Item = impl Borrow<Subpath<PointId>>>, transform: DAffine2, color: &str) {
		const PATTERN_WIDTH: usize = 4;
		const PATTERN_HEIGHT: usize = 4;

		let pattern_canvas = OffscreenCanvas::new(PATTERN_WIDTH as u32, PATTERN_HEIGHT as u32).unwrap();
		let pattern_context: OffscreenCanvasRenderingContext2d = pattern_canvas
			.get_context("2d")
			.ok()
			.flatten()
			.expect("Failed to get canvas context")
			.dyn_into()
			.expect("Context should be a canvas 2d context");

		// 4x4 pixels, 4 components (RGBA) per pixel
		let mut data = [0_u8; 4 * PATTERN_WIDTH * PATTERN_HEIGHT];

		let rgba = hex_to_rgba_u8(color);

		// ┌▄▄┬──┬──┬──┐
		// ├▀▀┼──┼──┼──┤
		// ├──┼──┼▄▄┼──┤
		// ├──┼──┼▀▀┼──┤
		// └──┴──┴──┴──┘
		let pixels = [(0, 0), (2, 2)];
		for &(x, y) in &pixels {
			let index = (x + y * PATTERN_WIDTH) * 4;
			data[index..index + 4].copy_from_slice(&rgba);
		}

		let image_data = web_sys::ImageData::new_with_u8_clamped_array_and_sh(wasm_bindgen::Clamped(&data), PATTERN_WIDTH as u32, PATTERN_HEIGHT as u32).unwrap();

View on GitHub (pinned to c507b35645)

Solutions

  1. Use dyn_ref::<OffscreenCanvasRenderingContext2d>() to try the downcast without panicking, logging and bailing on mismatch
  2. Pin consistent web-sys/wasm-bindgen versions so the Rust-side type matches the shipped JS classes
  3. Test on the oldest supported browser matrix to catch partial OffscreenCanvas implementations

Example fix

// before
.dyn_into()
.expect("Context should be a canvas 2d context");

// after
let Ok(pattern_context) = ctx.dyn_into::<OffscreenCanvasRenderingContext2d>() else {
	log::error!("2D context was not an OffscreenCanvasRenderingContext2d");
	return;
};
Defensive patterns

Strategy: type-guard

Type guard

fn as_offscreen_2d(value: &wasm_bindgen::JsValue) -> bool {
	value.is_instance_of::<web_sys::OffscreenCanvasRenderingContext2d>()
}

Try / catch

match ctx.dyn_into::<OffscreenCanvasRenderingContext2d>() {
	Ok(typed) => { /* use typed context */ }
	Err(value) => log::error!("unexpected 2D context type: {value:?}"),
}

Prevention

When it happens

Trigger: A browser or polyfill returns a non-OffscreenCanvas 2D context object from OffscreenCanvas.getContext('2d'), causing JsCast::dyn_into to fail and the expect to panic.

Common situations: Running the web editor under legacy polyfills (core-js/ OffscreenCanvas shims), WebViews with half-implemented OffscreenCanvas, or wasm-bindgen/web-sys version mismatches that change the expected JS class.

Related errors


AI-assisted analysis of GraphiteEditor/Graphite@c507b35645 (2026-08-16). Data as JSON: /api/errors/beea4b510d8e74ee. Report an issue: GitHub.