siyuan-note/siyuan · error · Error

Canvas 2D is unavailable

Error message

Canvas 2D is unavailable

What it means

GraphLabelRenderer constructor calls canvas.getContext('2d') and throws when it returns null. A null 2D context means the browser could not allocate a CanvasRenderingContext2D — typically because canvas contexts are exhausted, the canvas was already assigned an incompatible context type, or the environment lacks 2D canvas support entirely.

Source

Thrown at app/src/layout/dock/graph/labelRenderer.ts:16

import {IGraphRenderState} from "./renderer";

const LABEL_FONT_SIZE = 32;
const LABEL_CELL_WIDTH = 72;
const LABEL_CELL_HEIGHT = 20;

export class GraphLabelRenderer {
    private readonly canvas: HTMLCanvasElement;
    private readonly context: CanvasRenderingContext2D;
    private geometryVersion = -1;
    private labelOrder: number[] = [];

    constructor(canvas: HTMLCanvasElement) {
        const context = canvas.getContext("2d");
        if (!context) {
            throw new Error("Canvas 2D is unavailable");
        }
        this.canvas = canvas;
        this.context = context;
    }

    public render(state: IGraphRenderState) {
        const width = Math.max(1, Math.round(state.width * state.devicePixelRatio));
        const height = Math.max(1, Math.round(state.height * state.devicePixelRatio));
        if (this.canvas.width !== width || this.canvas.height !== height) {
            this.canvas.width = width;
            this.canvas.height = height;
        }
        if (this.geometryVersion !== state.geometryVersion) {
            this.labelOrder = state.data.nodes
                .filter((node) => Boolean(node.label))
                .sort((left, right) => right.degree - left.degree || left.index - right.index)
                .map((node) => node.index);
            this.geometryVersion = state.geometryVersion;

View on GitHub (pinned to 251596fc0d)

Solutions

  1. Use a fresh <canvas> element for the label renderer; do not share it with the WebGL renderer.
  2. Free prior canvas contexts (set canvas width/height to 0 and remove the element) before requesting a new one to avoid the per-page context limit.
  3. Feature-detect 2D canvas support before opening the graph dock and show a fallback message if unavailable.
  4. In tests, polyfill canvas (e.g. node-canvas / jest-canvas-mock) instead of running under jsdom without one.

Example fix

// before
const ctx = canvas.getContext('2d');
// after
const ctx = canvas.getContext('2d');
if (!ctx) { showMessage(window.siyuan.languages['No canvas support']); return null; }
Defensive patterns

Strategy: type-guard

Validate before calling

const supports2D = typeof HTMLCanvasElement !== 'undefined' &&
    typeof document.createElement('canvas').getContext('2d') === 'object';
if (!supports2D) { /* render fallback */ }

Type guard

function canvas2DSupported(): boolean {
    try { return !!document.createElement('canvas').getContext('2d'); }
    catch { return false; }
}

Try / catch

try { return new GraphLabelRenderer(canvas); }
catch (e) { showGraphFallback('2D canvas unavailable'); return null; }

Prevention

When it happens

Trigger: Constructing GraphLabelRenderer on a canvas that already obtained a webgl2 context (context type mismatch), in a headless/test environment without a real 2D canvas implementation, after leaking too many canvas contexts so the browser refuses new ones, or in a webview that disables 2D canvas.

Common situations: Old Android WebView or stripped-down Electron builds with canvas disabled; automated tests in jsdom (no real canvas); reused canvas element after webgl context; very long-running sessions that leaked offscreen canvases.

Related errors


AI-assisted analysis of siyuan-note/siyuan@251596fc0d (2026-08-12). Data as JSON: /api/errors/81d2bfee33ea990a. Report an issue: GitHub.