{"record":{"id":"fb3fba97b6823522","repo":"wailsapp/wails","slug":"initmainloop-was-not-called","errorCode":null,"errorMessage":"initMainLoop was not called","messagePattern":"initMainLoop was not called","errorType":"panic","errorClass":null,"httpStatus":null,"severity":"error","filePath":"v3/pkg/application/mainthread_windows.go","lineNumber":96,"sourceCode":"\tif m.invokeRequired() {\n\t\tw32.PostMessage(mainThreadHWND, wmInvokeCallback, uintptr(id), 0)\n\t} else {\n\t\tmainThreadFunctionStoreLock.Lock()\n\t\tfn := mainThreadFunctionStore[id]\n\t\tdelete(mainThreadFunctionStore, id)\n\t\tmainThreadFunctionStoreLock.Unlock()\n\n\t\tif fn == nil {\n\t\t\tFatal(\"dispatchOnMainThread called with invalid id: %v\", id)\n\t\t}\n\t\tfn()\n\t}\n}\n\nfunc (m *windowsApp) invokeRequired() bool {\n\tmainThreadID := m.mainThreadID\n\tif mainThreadID == 0 {\n\t\tpanic(\"initMainLoop was not called\")\n\t}\n\n\treturn mainThreadID != w32.GetCurrentThreadId()\n}\n\nfunc (m *windowsApp) invokeCallback(wParam, lParam uintptr) {\n\t// TODO: Should we invoke just one or all queued? In v2 we always invoked all pendings...\n\truntime.LockOSThread()\n\tdefer runtime.UnlockOSThread()\n\tif m.invokeRequired() {\n\t\tpanic(\"invokeCallback must always be called on the MainOSThread\")\n\t}\n\n\tmainThreadFunctionStoreLock.Lock()\n\tfnIDs := make([]uint, 0, len(mainThreadFunctionStore))\n\tfor id := range mainThreadFunctionStore {\n\t\tfnIDs = append(fnIDs, id)\n\t}","sourceCodeStart":78,"sourceCodeEnd":114,"githubUrl":"https://github.com/wailsapp/wails/blob/0e754b1b40ba9044c2a1460e23b7c3de20fc5cf3/v3/pkg/application/mainthread_windows.go#L78-L114","documentation":"On Windows, windowsApp.invokeRequired() compares the current thread ID against the main thread ID captured during main-loop initialization; if mainThreadID is still 0 it panics with 'initMainLoop was not called'. It means UI-thread affinity logic ran before the application main loop was initialized.","triggerScenarios":"Any code path that reaches invokeRequired() (e.g. invokeCallback or dispatch machinery) before the app's main loop initialized the main thread ID — typically invoking/dispatching work before app.Run() has started the main loop, or using windowing APIs in unit tests that never start the app.","commonSituations":"Calling dialog/notification/window helpers from an init() or from a service constructor that runs before the app starts; tests exercising Windows app code without booting the main loop; ordering changes in startup code after a v3 upgrade.","solutions":["Defer any UI or dispatch calls until after the application main loop is running (inside service Startup hooks or later)","In tests, initialize the main loop / use the app test harness instead of calling windows UI helpers directly","Review startup ordering so nothing touches invokeRequired before initMainLoop executes"],"exampleFix":"// before\nfunc NewApp() *App {\n    a := &App{}\n    showNotification() // dispatches before main loop: panics\n    return a\n}\n\n// after\nfunc (a *App) Startup(ctx context.Context) {\n    showNotification() // main loop running: safe\n}","handlingStrategy":"validation","validationCode":"// only dispatch UI work once the app is running\nif appStarted.Load() {\n    app.DispatchOnMainThread(fn)\n} else {\n    queueUntilStartup(fn) // drain inside your Startup hook\n}","typeGuard":null,"tryCatchPattern":null,"preventionTips":["Do not call window/dialog/notification APIs from init() or constructors","Move UI work into the service Startup hook or later lifecycle callbacks","In tests, boot the app test harness before touching Windows UI helpers"],"tags":["windows","lifecycle","main-thread","startup-order","panic","v3"],"backgroundTag":null,"analyzedSha":"0e754b1b40ba9044c2a1460e23b7c3de20fc5cf3","analyzedAt":"2026-08-15T14:17:36.034Z","schemaVersion":2},"datasetVersion":"2026-08-15T17:31:12.345Z"}