github/copilot-sdk · error · CanvasError

canvas_action_no_handler

canvas_action_no_handler

Error message

No handler implemented for this canvas action

What it means

When invoke() finds the canvas but no handler is registered under actionHandlers for params.actionName, it throws a CanvasError with code canvas_action_no_handler. The session distinguishes "unknown canvas" from "canvas exists but doesn't implement the action".

Solutions

  1. Add the missing entry to the canvas's actionHandlers map.
  2. Fix the actionName spelling/casing to match a registered handler.
  3. Update client and server sides to the same version so declared and implemented actions agree.

Example fix

// before
registerCanvas({ canvasId: "c1", actionHandlers: {} });
await canvas.invoke({ canvasId: "c1", actionName: "export" });
// after
registerCanvas({ canvasId: "c1", actionHandlers: { export: async (p) => ({}) } });
Defensive patterns

Strategy: validation

Validate before calling

const handlers = getActionHandlers(canvasId);
if (!handlers?.[actionName]) throw new Error(`Canvas "${canvasId}" has no handler for "${actionName}"`);

Try / catch

try { await canvas.invoke({ canvasId, actionName }); } catch (e) { if (e?.code === 'canvas_action_no_handler') console.error(`Implement handler "${actionName}" on canvas "${canvasId}"`); else throw e; }

Prevention

When it happens

Trigger: canvas.invoke({ canvasId, actionName }) where the canvas is registered but its actionHandlers map lacks actionName — typo'd action name, handler not implemented, or handler registered on a different canvas.

Common situations: Client shipped a new action before the canvas implementation registered it; version mismatch between client and session code; singular/plural or casing mismatch in action names.

Related errors


AI-assisted analysis of github/copilot-sdk@cd8cf15dc3 (2026-09-09). Data as JSON: /api/errors/e2e85060e614b36a. Report an issue: GitHub.

Appendix: source

Thrown at nodejs/src/session.ts:1417

                }
            },
            async close(params) {
                const canvas = self.canvases.get(params.canvasId);
                if (!canvas) throw new Error(`No canvas registered with id "${params.canvasId}"`);
                try {
                    if (canvas.onClose) {
                        await canvas.onClose(params);
                    }
                } catch (error) {
                    throw toCanvasRpcError(error);
                }
            },
            async invoke(params) {
                const canvas = self.canvases.get(params.canvasId);
                if (!canvas) throw new Error(`No canvas registered with id "${params.canvasId}"`);
                const handler = canvas.actionHandlers.get(params.actionName);
                if (!handler) {
                    throw new CanvasError(
                        "canvas_action_no_handler",
                        "No handler implemented for this canvas action"
                    );
                }
                try {
                    return (await handler(params)) as CanvasActionInvokeResult;
                } catch (error) {
                    throw toCanvasRpcError(error);
                }
            },
        };
    }

    /**
     * Registers factory closures and reverse-RPC handlers for this session.
     *
     * @param factories - Factory handles declared by the joining extension.
     * @internal Called by the SDK when an extension joins a session.

View on GitHub (pinned to cd8cf15dc3)