wailsapp/wails · error

Create canvas from %v failed.

Error message

Create canvas from %v failed.

What it means

NewCanvasFromHwnd calls GetDC(hwnd) and panics when it returns NULL. GetDC fails when the window handle is invalid/destroyed, when the window belongs to a different process with restricted access, or when the process has exhausted its DC handle quota (the classic 10000-handle GDI limit, or per-window DC cache pressure). The panic message includes the offending hwnd value for diagnosis.

Source

Thrown at v2/internal/frontend/desktop/windows/winc/canvas.go:27

import (
	"fmt"

	"github.com/wailsapp/wails/v2/internal/frontend/desktop/windows/winc/w32"
)

type Canvas struct {
	hwnd         w32.HWND
	hdc          w32.HDC
	doNotDispose bool
}

var nullBrush = NewNullBrush()

func NewCanvasFromHwnd(hwnd w32.HWND) *Canvas {
	hdc := w32.GetDC(hwnd)
	if hdc == 0 {
		panic(fmt.Sprintf("Create canvas from %v failed.", hwnd))
	}

	return &Canvas{hwnd: hwnd, hdc: hdc, doNotDispose: false}
}

func NewCanvasFromHDC(hdc w32.HDC) *Canvas {
	if hdc == 0 {
		panic("Cannot create canvas from invalid HDC.")
	}

	return &Canvas{hdc: hdc, doNotDispose: true}
}

func (ca *Canvas) Dispose() {
	if !ca.doNotDispose && ca.hdc != 0 {
		if ca.hwnd == 0 {
			w32.DeleteDC(ca.hdc)
		} else {

View on GitHub (pinned to 0e754b1b40)

Solutions

  1. Always pair NewCanvasFromHwnd with canvas.Dispose() (defer it) so the DC is returned via ReleaseDC
  2. Verify the control is still alive: check w32.IsWindow(hwnd) before creating the canvas in async/delayed paint paths
  3. If DCs are being leaked, cache one Canvas for the control's lifetime instead of per-paint creation
  4. Capture the hwnd printed in the panic message and confirm in Spy++ whether it is a live, valid window of this process

Example fix

// before
func (c *Meter) tick() {
    ca := winc.NewCanvasFromHwnd(c.Handle()) // new DC every tick, never released -> GetDC eventually returns 0
    c.draw(ca)
}

// after
func (c *Meter) tick() {
    if !w32.IsWindow(c.Handle()) { return }
    ca := winc.NewCanvasFromHwnd(c.Handle())
    defer ca.Dispose()
    c.draw(ca)
}
Defensive patterns

Strategy: validation

Validate before calling

func canvasFor(hwnd w32.HWND) *winc.Canvas {
    if !w32.IsWindow(hwnd) { return nil } // GetDC on a dead HWND can return 0
    ca := winc.NewCanvasFromHwnd(hwnd)
    if ca == nil { return nil }
    return ca
}
// Always: ca := canvasFor(h); if ca != nil { defer ca.Dispose() }

Prevention

When it happens

Trigger: Calling NewCanvasFromHwnd on a control whose HWND was already destroyed (e.g. after WM_DESTROY, in a deferred repaint callback); passing an HWND obtained from a closed window; heavy per-frame canvas creation leaking DCs because Dispose() was not called (GetDC/ReleaseDC pairs are bounded).

Common situations: Animation loops in custom winc controls that create a Canvas every tick without deferring canvas.Dispose(); drawing triggered from goroutines after the window closed; DC leak elsewhere consuming the handle quota.

Related errors


AI-assisted analysis of wailsapp/wails@0e754b1b40 (2026-08-15). Data as JSON: /api/errors/8f903bc3a50b7f9c. Report an issue: GitHub.