Quick answer: Always call handle.Complete() on the JobHandle before disposing the NativeArrays the job uses. Or mark fields [DeallocateOnJobCompletion] so the job system handles cleanup. Persistent allocations must be disposed in OnDestroy.

Here is how to fix Unity throwing InvalidOperationException: The Unity.Collections.NativeArray has been deallocated when your job tries to write to it. You scheduled a job, kept a reference to the input array, and disposed it before the job finished. Or you forgot to dispose at all and the leak detector trips on shutdown. Both share the same fix: explicit lifecycle for native containers.

The Symptom

Console error: InvalidOperationException: The Unity.Collections.NativeArray has been deallocated, it is not allowed to access it. Or in Editor.log on quit: A Native Collection has not been disposed, resulting in a memory leak. The job runs once or twice fine then errors on a subsequent frame.

What Causes This

Dispose before Complete. Calling Dispose on a NativeArray that is still in flight inside a job triggers the safety system error.

Forgetting to Complete. A job scheduled and forgotten lingers; if the array goes out of scope without dispose, the leak detector flags it.

Wrong allocator for DeallocateOnJobCompletion. The attribute only works with TempJob allocator. With Persistent, the auto-dispose does not trigger.

Job depending on already-disposed handle. Chaining a new job onto a JobHandle that completed and disposed its dependencies leaves the new job dependent on freed memory.

The Fix

Step 1: Always Complete before Dispose.

using Unity.Collections;
using Unity.Jobs;

public class RunJob : MonoBehaviour
{
    private NativeArray<float> data;
    private JobHandle handle;

    void Update()
    {
        data = new NativeArray<float>(1024, Allocator.TempJob);

        var job = new WriteJob { output = data };
        handle = job.Schedule(data.Length, 64);

        handle.Complete();   // MUST happen before dispose

        Debug.Log(data[0]);
        data.Dispose();
    }
}

Step 2: Use DeallocateOnJobCompletion for one-shot temps.

[BurstCompile]
public struct WriteJob : IJobParallelFor
{
    [DeallocateOnJobCompletion]
    public NativeArray<float> output;

    public void Execute(int i)
    {
        output[i] = i * 0.5f;
    }
}

The job system disposes output automatically after the job completes. No manual Dispose needed.

Step 3: For persistent allocations, dispose in OnDestroy.

public class PersistentNativeOwner : MonoBehaviour
{
    private NativeArray<int> persistent;

    void Awake()
    {
        persistent = new NativeArray<int>(1024, Allocator.Persistent);
    }

    void OnDestroy()
    {
        if (persistent.IsCreated)
        {
            persistent.Dispose();
        }
    }
}

Always check IsCreated before dispose to handle the case where Awake never ran (component disabled).

Step 4: Chain dependencies safely.

JobHandle h1 = job1.Schedule(arr.Length, 64);
JobHandle h2 = job2.Schedule(arr.Length, 64, h1);   // h2 depends on h1
h2.Complete();   // completes both
arr.Dispose();

Step 5: Enable Jobs Debugger to catch issues early. Open Jobs → JobsDebugger and enable. The safety system catches misuse and reports source locations. Disable for shipping builds (it is editor-only by default).

Avoiding Leaks On Application Quit

For singletons holding native arrays, hook Application.quitting:

void Awake()
{
    Application.quitting += () => {
        if (data.IsCreated) data.Dispose();
    };
}

“Jobs run async. Native containers are sync. Bridge them with Complete or DeallocateOnJobCompletion.”

Related Issues

For Burst compile errors, see IJobParallelFor Burst Error. For ECS aspects, see ECS Aspects Not Registering.

Complete before Dispose. DeallocateOnJobCompletion for temps. OnDestroy for persistent. No leaks.