{"record":{"id":"8f903bc3a50b7f9c","repo":"wailsapp/wails","slug":"create-canvas-from-v-failed","errorCode":null,"errorMessage":"Create canvas from %v failed.","messagePattern":"Create canvas from (.+?) failed\\.","errorType":"panic","errorClass":null,"httpStatus":null,"severity":"error","filePath":"v2/internal/frontend/desktop/windows/winc/canvas.go","lineNumber":27,"sourceCode":"\nimport (\n\t\"fmt\"\n\n\t\"github.com/wailsapp/wails/v2/internal/frontend/desktop/windows/winc/w32\"\n)\n\ntype Canvas struct {\n\thwnd         w32.HWND\n\thdc          w32.HDC\n\tdoNotDispose bool\n}\n\nvar nullBrush = NewNullBrush()\n\nfunc NewCanvasFromHwnd(hwnd w32.HWND) *Canvas {\n\thdc := w32.GetDC(hwnd)\n\tif hdc == 0 {\n\t\tpanic(fmt.Sprintf(\"Create canvas from %v failed.\", hwnd))\n\t}\n\n\treturn &Canvas{hwnd: hwnd, hdc: hdc, doNotDispose: false}\n}\n\nfunc NewCanvasFromHDC(hdc w32.HDC) *Canvas {\n\tif hdc == 0 {\n\t\tpanic(\"Cannot create canvas from invalid HDC.\")\n\t}\n\n\treturn &Canvas{hdc: hdc, doNotDispose: true}\n}\n\nfunc (ca *Canvas) Dispose() {\n\tif !ca.doNotDispose && ca.hdc != 0 {\n\t\tif ca.hwnd == 0 {\n\t\t\tw32.DeleteDC(ca.hdc)\n\t\t} else {","sourceCodeStart":9,"sourceCodeEnd":45,"githubUrl":"https://github.com/wailsapp/wails/blob/0e754b1b40ba9044c2a1460e23b7c3de20fc5cf3/v2/internal/frontend/desktop/windows/winc/canvas.go#L9-L45","documentation":"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.","triggerScenarios":"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).","commonSituations":"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.","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"],"exampleFix":"// before\nfunc (c *Meter) tick() {\n    ca := winc.NewCanvasFromHwnd(c.Handle()) // new DC every tick, never released -> GetDC eventually returns 0\n    c.draw(ca)\n}\n\n// after\nfunc (c *Meter) tick() {\n    if !w32.IsWindow(c.Handle()) { return }\n    ca := winc.NewCanvasFromHwnd(c.Handle())\n    defer ca.Dispose()\n    c.draw(ca)\n}","handlingStrategy":"validation","validationCode":"func canvasFor(hwnd w32.HWND) *winc.Canvas {\n    if !w32.IsWindow(hwnd) { return nil } // GetDC on a dead HWND can return 0\n    ca := winc.NewCanvasFromHwnd(hwnd)\n    if ca == nil { return nil }\n    return ca\n}\n// Always: ca := canvasFor(h); if ca != nil { defer ca.Dispose() }","typeGuard":null,"tryCatchPattern":null,"preventionTips":["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"],"tags":["windows","gdi","device-context","resource-leak","winc","panic"],"backgroundTag":null,"analyzedSha":"0e754b1b40ba9044c2a1460e23b7c3de20fc5cf3","analyzedAt":"2026-08-15T14:17:36.034Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}