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

  1. 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
  2. Batch refreshes: remove+reinsert atomically rather than deleting items while processing click notifications
  3. 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

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


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