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
- Always pair NewCanvasFromHwnd with canvas.Dispose() (defer it) so the DC is returned via ReleaseDC
- Verify the control is still alive: check w32.IsWindow(hwnd) before creating the canvas in async/delayed paint paths
- If DCs are being leaked, cache one Canvas for the control's lifetime instead of per-paint creation
- 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
- defer canvas.Dispose() immediately after every NewCanvasFromHwnd
- Check w32.IsWindow before painting from timers/goroutines
- Cache a control-lifetime canvas instead of per-frame creation
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
- Faild to create solid color brush
- Failed to create null brush
- Cannot create canvas from invalid HDC.
- CreateFontIndirect failed
- GetSysColorBrush failed
AI-assisted analysis of wailsapp/wails@0e754b1b40 (2026-08-15).
Data as JSON: /api/errors/8f903bc3a50b7f9c.
Report an issue: GitHub.