app.exit() crashes on macOS due to std::exit() racing with [NSApp terminate:]

Author: iparnamaaCreated Aug 28, 2026Updated Sep 1, 2026

Bug

On macOS, calling app.exit() (or the internal window::_close() it wraps) crashes the app. The workaround is to call app.killProcess() instead, which does not go through this code path.

Where

lib/webview/webview.h, cocoa_wkwebview_engine::terminate(), line 793:

cpp
void terminate(int exitCode = 0) {
  close();
  ((void (*)(id, SEL, id))objc_msgSend)("NSApp"_cls, "terminate:"_sel,
                                        nullptr);
  std::exit(exitCode);
}

Why it crashes

run() blocks the main thread inside [NSApplication run]. [NSApp terminate:] asks Cocoa to shut down, but this is asynchronous — it does not stop the run loop immediately, it schedules the shutdown.

std::exit(exitCode) runs on the very next line, with no wait. This tears down the process while Cocoa is still in the middle of stopping the run loop it was told to stop, racing with its own teardown. That race is what crashes.

Compare with the other two backends, which do not have this problem:

  • GTK (Linux)terminate() just destroys the window; no std::exit() at all. gtk_main() ends on its own once the window is destroyed, and the process exits normally afterward.
  • Win32terminate() destroys the window and waits (WaitForSingleObject(evtWindowClosed, 300)) before returning. No std::exit() here either; ExitProcess is called later, from run(), once a WM_QUIT message arrives.

macOS is the only backend that force-exits immediately after asking to quit, without waiting for that quit to actually happen.

Suggested fix

[NSApp terminate:] already ends the process itself once Cocoa finishes tearing down — that is standard Cocoa app-lifecycle behavior. The extra std::exit() call is not needed and is what causes the race. Simplest fix:

cpp
void terminate(int exitCode = 0) {
  close();
  ((void (*)(id, SEL, id))objc_msgSend)("NSApp"_cls, "terminate:"_sel,
                                        nullptr);
  // [NSApp terminate:] ends the process itself once Cocoa finishes
  // tearing down the run loop. Calling std::exit() immediately after,
  // with no wait, races with that teardown and crashes.
}

If the exact exitCode must reach the process exit status, an alternative closer to the Windows fix is to wait for termination to complete before exiting, rather than exiting unconditionally on the next line.

Repro / workaround

We hit this in our own Neutralino app (window mode) — every call to app.exit() on macOS crashes on quit. app.killProcess() (which sends SIGINT to the process directly, bypassing terminate()) does not crash, and is the workaround we're using in the meantime. This also affects app.restartProcess(), since that helper calls app.exit() internally after spawning the replacement process.

Environment

  • neutralinojs (this repo), main branch as of this report
  • Observed on macOS, window mode

Source: neutralinojs/neutralinojs