Quick answer: Stop your background threads before OnApplicationQuit returns, null out native plugin handles in OnDestroy, and avoid touching Unity APIs from finalizers. Crashes on exit are almost always a race between managed shutdown and native resource destruction.
Here is how to fix Unity crashes on exit. Gameplay is fine, saves work, menus are responsive. The player clicks Quit (or presses Alt+F4, or closes the window) and for a split second everything looks normal — then Windows pops up “YourGame.exe has stopped working” or the game window just vanishes without a clean close. The player log ends abruptly mid-shutdown or shows a native crash in Unity’s cleanup code. This class of bug has a reputation for being “cosmetic” but it absolutely is not — quit crashes fail cert, trigger crash reports to Steam and consoles, and prevent your final save from flushing.
The Symptom
The game crashes during or immediately after the user initiates quit. You see one or more of: a Windows Error Reporting dialog, an abrupt process termination with no dialog, a hang where the process lingers in Task Manager for seconds before disappearing, or a crash dump in %LOCALAPPDATA%\CrashDumps. The Player.log ends mid-sentence, often after OnApplicationQuit logs but before Unity’s subsystem shutdown completes.
Stack traces from the crash dump typically point into native code — Unity’s graphics device shutdown, plugin unload, or the mono runtime finalizer thread. Rarely does the crash point at your own managed code, which makes it frustrating to diagnose.
What Causes This
Background threads still running. If you spawn a Thread or Task that does not stop when the game quits, it can access destroyed Unity objects or native resources mid-shutdown. The main thread is tearing down the graphics device while your background thread is trying to call an API that internally touches the graphics device. Kaboom.
Native plugin lifetime mismatch. A native plugin (P/Invoke DLL) holds state that must be cleaned up in a specific order. If your OnApplicationQuit calls the plugin’s shutdown function but Unity has already unloaded the DLL, you get an access violation. Conversely, if a finalizer on a managed wrapper runs after the plugin is unloaded, it calls into freed memory.
Static references to destroyed MonoBehaviours. A static field holding a reference to a MonoBehaviour can be accessed during shutdown by another script’s OnDestroy. But the referenced MonoBehaviour may already have been destroyed by that point. Accessing properties on it returns “fake null” (Unity’s overloaded equality) that looks like a valid reference but is actually destroyed.
File handles and streams not closed. If you have open FileStream, StreamWriter, or database connections, finalizers attempt to close them during process shutdown. If the finalizer runs after Unity has destroyed its I/O subsystem, or if the file was on a network drive that is no longer reachable, the finalizer can stall or crash.
Third-party SDKs. Analytics SDKs, ad SDKs, and multiplayer SDKs frequently hold native resources that must shut down cleanly. SDKs updated for one Unity version may crash on quit in another Unity version. Check SDK release notes for known shutdown issues.
The Fix
Step 1: Stop threads in OnApplicationQuit. Any background thread or cancellation token source should be stopped before OnApplicationQuit returns. Use a CancellationTokenSource and signal cancellation, then join the thread with a timeout.
using System.Threading;
using UnityEngine;
public class BackgroundWorker : MonoBehaviour
{
private CancellationTokenSource cts;
private Thread worker;
void Start()
{
cts = new CancellationTokenSource();
worker = new Thread(() => DoWork(cts.Token));
worker.IsBackground = true;
worker.Start();
}
void OnApplicationQuit()
{
cts?.Cancel();
worker?.Join(500); // Wait up to 500ms
cts?.Dispose();
}
private static void DoWork(CancellationToken token)
{
while (!token.IsCancellationRequested)
{
// Periodically check cancellation
Thread.Sleep(10);
}
}
}
The IsBackground = true flag is important — without it, the thread keeps the process alive after Unity exits. With it, the thread is killed automatically if it does not stop in time. Combined with a proper cancellation signal, you get clean shutdown in the common case and a safety net for the uncommon case.
Step 2: Null out native handles. If you hold pointers from a native plugin, set them to zero in OnApplicationQuit or OnDestroy, and check for zero before every use. Do not rely on finalizers — they run on a different thread and in unpredictable order.
private IntPtr nativeHandle;
void OnApplicationQuit()
{
if (nativeHandle != IntPtr.Zero)
{
NativePlugin.Shutdown(nativeHandle);
nativeHandle = IntPtr.Zero;
}
}
Step 3: Avoid Unity API calls in finalizers. If you have a class with a finalizer (~MyClass()), do not call any Unity API from it. Finalizers run on a separate thread and after Unity may have torn down its main thread state. Use IDisposable and explicit disposal instead, and if you must have a finalizer as a safety net, only release native resources in it — nothing Unity-related.
Step 4: Order execution explicitly. Use Unity’s Script Execution Order settings (Edit > Project Settings > Script Execution Order) to ensure your shutdown-sensitive scripts run their OnApplicationQuit first. For native plugin wrappers, set their execution order to a low negative number so they quit before anything that might reference them.
Debugging Technique
Add a progress log in every OnApplicationQuit and OnDestroy so you can see exactly how far shutdown gets before the crash.
void OnApplicationQuit()
{
Debug.Log($"[Quit] {GetType().Name} starting shutdown");
// ... shutdown work ...
Debug.Log($"[Quit] {GetType().Name} shutdown complete");
}
After a crash, the last log line tells you the exact component where shutdown stopped. The component that did not finish logging is the suspect.
Platform-Specific Notes
On Windows, crash dumps land in %LOCALAPPDATA%\CrashDumps\YourGame.exe.####.dmp. Open them in Visual Studio with matching symbols for a readable stack trace.
On macOS, crash reports are in ~/Library/Logs/DiagnosticReports/.
On consoles, crash dumps go to the platform’s crash reporting service and can fail cert if quit crashes exceed the platform threshold. Treat quit crashes as P0 bugs before submitting for cert.
“Shutdown is the one moment where order matters more than anywhere else. Spend five minutes writing it correctly or spend a day debugging a crash that only happens when the game is closing.”
Related Issues
For crash diagnostics generally, see Capture and Symbolicate Crash Dumps and Understanding Unity Stack Traces. For Android-specific shutdown issues, check Unity Build Crashes on Android.
A crash on exit is still a crash. Fix them before shipping.