wailsapp/wails · error

Create canvas from 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.

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.

Appendix: 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)