Quick answer: IL2CPP converts C# to C++ ahead of time, which means it cannot handle certain dynamic code patterns that Mono's JIT compiler supports.
Here is how to fix Unity il2cpp build errors. Your project builds and runs perfectly with the Mono scripting backend, but the moment you switch to IL2CPP the build fails with cryptic C++ compiler errors, or worse, it builds but crashes at runtime with ExecutionEngineException. IL2CPP is required for iOS and consoles and strongly recommended for Android and desktop releases, so you cannot just avoid it. This guide explains why IL2CPP breaks things that Mono handles fine and how to fix every common failure pattern.
IL2CPP vs Mono: Why Things Break
Mono uses a Just-In-Time (JIT) compiler that converts C# bytecode to machine code at runtime. This means Mono can handle dynamic code patterns—creating types via reflection, constructing generic types it has never seen before, loading assemblies at runtime. It figures things out as it goes.
IL2CPP is an Ahead-Of-Time (AOT) compiler. It converts your C# code to C++ during the build, then compiles the C++ to native machine code. Everything must be known at build time. If IL2CPP does not see a type, a method, or a specific generic instantiation during the build, it does not generate code for it. At runtime, when your code tries to use that missing piece, it crashes.
The three main categories of IL2CPP failures are:
Build-time errors: The C++ compiler rejects the generated code. These show up as unresolved symbols, type mismatches, or compilation failures in the build log.
Stripping errors: Managed code stripping removes types or methods that are actually needed, causing TypeLoadException, MissingMethodException, or null references at runtime.
AOT errors: Generic type instantiations that were not anticipated at build time cause ExecutionEngineException at runtime.
Managed Code Stripping and link.xml
Unity’s managed code stripper analyzes your code at build time and removes types and methods that appear unused. This reduces build size significantly but can remove things that are actually needed—especially types accessed only through reflection, serialization, or string-based lookups.
The stripping level is configured in Player Settings → Other Settings → Managed Stripping Level:
// Stripping levels (least to most aggressive)
Minimal // Strips very little — safest, largest builds
Low // Strips unused types in non-Unity assemblies
Medium // Strips unused types in all assemblies
High // Strips unused types AND unused members
If your build works with Minimal stripping but fails with High, you have a stripping issue. The fix is a link.xml file that tells the stripper to preserve specific types. Create this file in your Assets folder:
<!-- Assets/link.xml -->
<linker>
<!-- Preserve an entire assembly -->
<assembly fullname="MyPlugin" preserve="all"/>
<!-- Preserve a specific type -->
<assembly fullname="Assembly-CSharp">
<type fullname="MyGame.SaveData" preserve="all"/>
<type fullname="MyGame.Config.SettingsData" preserve="all"/>
</assembly>
<!-- Preserve all types in a namespace -->
<assembly fullname="Assembly-CSharp">
<type fullname="MyGame.Networking" preserve="all"/>
</assembly>
<!-- Preserve Newtonsoft.Json if using it for serialization -->
<assembly fullname="Newtonsoft.Json" preserve="all"/>
</linker>
Types that commonly need preservation include: serialization data classes (JSON, XML), network message types, types created via Activator.CreateInstance(), and types accessed via Type.GetType() with a string name.
The [Preserve] Attribute
For types you control, you can use the [Preserve] attribute instead of link.xml. This is more convenient for individual classes because the preservation directive lives with the code:
using UnityEngine.Scripting;
// Preserve the entire class and all its members
[Preserve]
public class NetworkMessage
{
public string type;
public string payload;
public long timestamp;
}
// Or preserve just specific members
public class Analytics
{
[Preserve]
public static void TrackEvent(string name, Dictionary<string, object> data)
{
// This method is called via reflection by the analytics SDK
}
}
The [Preserve] attribute comes from UnityEngine.Scripting. It tells the linker to keep the decorated type or member regardless of whether it appears to be used. Use this for any code that is invoked via reflection, attribute-based systems, or string-based method calls.
Generic Type AOT Errors
IL2CPP must generate C++ code for every specific generic type instantiation. If your code creates a List<MyCustomType> somewhere that IL2CPP can analyze, it will generate the code. But if the generic type is constructed dynamically—for example, through a deserialization framework that creates List<T> where T is determined at runtime—IL2CPP will not know to generate it.
The symptom is an ExecutionEngineException at runtime, often with a message about a missing AOT compilation for a specific generic instantiation.
The fix is to create an “AOT hint” method that explicitly references every generic instantiation you need:
using System.Collections.Generic;
using UnityEngine.Scripting;
// This class exists solely to force IL2CPP to generate code
// for generic types used via reflection or serialization
public static class AOTHints
{
// This method is never called — it just needs to exist
[Preserve]
private static void EnsureGenericTypes()
{
// Force IL2CPP to generate code for these generic types
_ = new List<SaveSlot>();
_ = new Dictionary<string, PlayerStats>();
_ = new List<InventoryItem>();
_ = new Dictionary<int, QuestState>();
_ = new HashSet<AchievementId>();
// Also force comparer types if using sorted collections
_ = EqualityComparer<AchievementId>.Default;
_ = Comparer<PlayerStats>.Default;
}
}
The method never needs to be called—its mere existence causes IL2CPP to analyze it during the build and generate the necessary C++ code for each generic instantiation. The [Preserve] attribute prevents the stripper from removing the unused method.
Reading the IL2CPP Build Log
When an IL2CPP build fails, the error message in the Unity console is often a truncated summary. The full error is in the build log. Find it at:
// Windows
C:\Users\YourName\AppData\Local\Unity\Editor\Editor.log
// macOS
~/Library/Logs/Unity/Editor.log
// Or in the project's Library folder
Library/Bee/artifacts/ // Contains il2cpp output and C++ compiler errors
Search for il2cpp or error C (for MSVC errors) or error: (for Clang errors) in the log. Common patterns include:
unresolved external symbol — A native function (P/Invoke) is declared but the native library is missing or has a different symbol name. Check your DllImport attributes and ensure the native library is included for the target platform.
cannot convert from X to Y — A type mismatch in generated C++ code, often caused by unsafe code or pointer operations that IL2CPP handles differently than Mono.
Build completed with errors after running for minutes — If the build runs for a long time before failing, it is likely a C++ compilation error in the generated code. The actual error is often buried thousands of lines before the final failure message.
“IL2CPP errors feel hostile because you are reading C++ compiler output for code you wrote in C#. But the error almost always maps to a specific C# pattern. Once you learn the five or six common patterns, IL2CPP stops being scary and becomes just another build step to manage.”
Preventing IL2CPP Issues Proactively
The best approach to IL2CPP is to build with it regularly during development rather than waiting until the final release. Set up a CI pipeline that does an IL2CPP build on every merge to main. This catches stripping and AOT issues early, when the cause is obvious (the most recent change).
Other proactive measures:
Avoid System.Reflection.Emit entirely. IL2CPP does not support runtime code generation. Libraries that use Emit (some older serialization frameworks, some dependency injection containers) will fail. Check your plugin documentation for IL2CPP compatibility.
Prefer JsonUtility or source-generated serializers over reflection-based ones. Unity’s built-in JsonUtility does not use reflection and works perfectly with IL2CPP. If you use Newtonsoft.Json, add it to link.xml and test early.
Test on actual target hardware. Some IL2CPP issues only manifest on specific platforms. An IL2CPP build that works on Windows may crash on Android ARM64 due to alignment issues or different native calling conventions.
Keep stripping at Medium as a default during development. This catches most stripping issues without being as aggressive as High. Switch to High only after you have established a thorough link.xml.
Related Issues
If your IL2CPP build succeeds but crashes on iOS, see Fix: Unity IL2CPP Build Crash on iOS. For build errors related to missing scenes or resources rather than code, check Fix: Unity Build Missing Scenes or Resources. For tracking platform-specific build issues across your team, read Bug Reporting Tools for Unity Developers.
Build with IL2CPP early and often. Every week you wait is a week of code that might use patterns IL2CPP cannot handle. link.xml and [Preserve] are your tools for telling IL2CPP what to keep. Use them liberally and test on real devices.