wailsapp/wails · error
SendMessage(LVM_UPDATE)
Error message
SendMessage(LVM_UPDATE)
What it means
In the list-view's WM_NOTIFY handler for NM_CLICK, when the click hits a state icon (LVHT_ONITEMSTATEICON) the code toggles the item's checked state and then sends LVM_UPDATE to repaint that item, panicking if the message returns FALSE. LVM_UPDATE fails when the item index is out of range or the control is being destroyed mid-notification. In this path the index comes from a fresh hit test, so the realistic trigger is teardown racing the click, or an item being removed concurrently so the hit-test index no longer exists.
Source
Thrown at v2/internal/frontend/desktop/windows/winc/listview.go:484
}
case w32.NM_DBLCLK:
lv.onDoubleClick.Fire(NewEvent(lv, nil))
case w32.NM_CLICK:
ac := (*w32.NMITEMACTIVATE)(unsafe.Pointer(lparam))
var hti w32.LVHITTESTINFO
hti.Pt = w32.POINT{ac.PtAction.X, ac.PtAction.Y}
w32.SendMessage(lv.hwnd, w32.LVM_HITTEST, 0, uintptr(unsafe.Pointer(&hti)))
if hti.Flags == w32.LVHT_ONITEMSTATEICON {
if item := lv.findItemByIndex(int(hti.IItem)); item != nil {
if item, ok := item.(ListItemChecker); ok {
checked := !item.Checked()
item.SetChecked(checked)
lv.onCheckChanged.Fire(NewEvent(lv, item))
if w32.SendMessage(lv.hwnd, w32.LVM_UPDATE, uintptr(hti.IItem), 0) == w32.FALSE {
panic("SendMessage(LVM_UPDATE)")
}
}
}
}
hti.Pt = w32.POINT{ac.PtAction.X, ac.PtAction.Y}
w32.SendMessage(lv.hwnd, w32.LVM_SUBITEMHITTEST, 0, uintptr(unsafe.Pointer(&hti)))
lv.onClick.Fire(NewEvent(lv, hti.ISubItem))
case w32.LVN_KEYDOWN:
nmkey := (*w32.NMLVKEYDOWN)(unsafe.Pointer(lparam))
if nmkey.WVKey == w32.VK_SPACE && lv.CheckBoxes() {
if item := lv.SelectedItem(); item != nil {
if item, ok := item.(ListItemChecker); ok {
checked := !item.Checked()
item.SetChecked(checked)
lv.onCheckChanged.Fire(NewEvent(lv, item))
}View on GitHub (pinned to 0e754b1b40)
Solutions
- Mutate ListView items only on the UI thread (marshal updates with Form.Synchronize or PostMessage) so item indexes can't change under an in-flight notification
- Batch refreshes: remove+reinsert atomically rather than deleting items while processing click notifications
- On close, stop background updaters (cancel their context) before destroying the window so no click can race teardown
Example fix
// before
go func() {
for upd := range ch {
lv.RemoveItemByName(...) // background goroutine mutates items mid-click -> LVM_UPDATE FALSE -> panic
}
}()
// after
for upd := range ch {
upd := upd
form.Synchronize(func() { // serialize with the notification handler on the UI thread
lv.RemoveItemByName(...)
})
} Defensive patterns
Strategy: validation
Validate before calling
// Serialize item mutations with the UI thread so click handlers never see stale indexes:
func (f *MainForm) updateItems(fn func()) {
f.Synchronize(fn) // runs on the UI thread, after in-flight notifications complete
} Prevention
- Mutate ListView items only via the UI thread (Synchronize/PostMessage)
- Cancel background refresh jobs before closing the window
- Avoid removing items inside handlers that fire during click processing
When it happens
Trigger: User clicks a checkbox in a ListView whose items are being mutated from another goroutine (delete/refresh between the hit test and LVM_UPDATE); the form is destroyed while the click notification is still processing; the hit-test returned LVHT_ONITEMSTATEICON with a stale/garbage IItem.
Common situations: ListViews refreshed from background data (polling, websocket pushes) while the user interacts; rapid close-during-click at shutdown.
Related errors
- SendMessage(LVM_SETCALLBACKMASK)
- cannot create window for " + className
- failed SetMenu
- no focused element to type into; pass a selector or click a
- GetSysColorBrush failed
AI-assisted analysis of wailsapp/wails@0e754b1b40 (2026-08-15).
Data as JSON: /api/errors/8ed4cff9d0dac1b4.
Report an issue: GitHub.