Archive

Posts Tagged ‘java’

The Misleading IndexOutOfRangeException That Means “Your List Isn’t Thread-Safe”

If you’ve ever seen a stack trace like this in a .NET application:

System.IndexOutOfRangeException: Index was outside the bounds of the array.
at System.Collections.Generic.List`1.Enumerator.MoveNext()
at System.Linq.Enumerable.WhereListIterator`1.MoveNext()
at System.Collections.Generic.List`1..ctor(IEnumerable`1 collection)
at System.Linq.Enumerable.ToList[TSource](IEnumerable`1 source)
at YourCode.SomeMethod(...)

…and you stared at the offending line — something innocuous like myList.Where(x => x.IsActive).ToList() — wondering how on earth a LINQ query could be indexing outside an array, you’ve run into one of the most misleadingly-named exceptions in the framework.

The message says “index out of bounds.” The actual problem is a race condition.

What the stack trace is telling you

Read the frames from the bottom up. Your code called ToList(). ToList() calls the List<T> constructor that takes an IEnumerable<T>. That constructor enumerates the Where iterator, which in turn enumerates the underlying List<T> via its Enumerator. And it’s Enumerator.MoveNext() that throws.

So the iterator is walking your list, and at some point it tries to read element N, and N is past the end of the internal array.

How can that happen? The list got smaller — or its internal buffer got swapped — while the enumerator was mid-walk.

List<T> is implemented as a wrapper around a T[] array plus a _size field. The enumerator captures a reference to the list at construction time, then on each MoveNext() it increments an index and reads list._items[index]. If another thread calls Remove, Clear, or triggers a resize via Add between two MoveNext() calls, the array your enumerator is reading from may have been replaced with a smaller one, or the items may have been shuffled. The result: an index that was valid when you started iterating is no longer valid, and you get IndexOutOfRangeException.

Why you don’t get the “nice” exception

List<T> does have a version-check mechanism. Every mutation increments an internal _version field, and the enumerator records the version it started with. If MoveNext() notices the version has changed, it throws InvalidOperationException: Collection was modified; enumeration operation may not execute — the exception most .NET developers have seen at least once and immediately recognise as a concurrency or mid-loop-mutation bug.

The catch: that version check happens after the index increment and array access. If the racing thread mutates the list in a way that shrinks the array between those two operations, you hit the raw IndexOutOfRangeException before the version check ever runs. Same root cause, much worse error message.

This is also why the bug is so frustrating to reproduce. It depends on precise interleaving of two threads down to the instruction level. You can hammer it in a test loop and not see it for a million iterations, then it triggers twice in five minutes in production.

The minimal reproduction

var list = new List<int>();
for (int i = 0; i < 1000; i++) list.Add(i);
// Thread A — repeatedly enumerate
Task.Run(() =>
{
while (true)
{
var copy = list.Where(x => x > 0).ToList();
}
});
// Thread B — repeatedly mutate
Task.Run(() =>
{
var rng = new Random();
while (true)
{
if (list.Count > 0) list.RemoveAt(rng.Next(list.Count));
list.Add(rng.Next());
}
});

Run that for a few seconds and you’ll get either InvalidOperationException or IndexOutOfRangeException — sometimes both, on different runs. The exception you get is essentially a coin flip determined by exactly when the second thread’s mutation lands relative to the first thread’s bounds check.

The diagnostic giveaway

The single most useful signal in the stack trace is this frame:

   at System.Collections.Generic.List`1.Enumerator.MoveNext()

IndexOutOfRangeException originating from List<T>.Enumerator.MoveNext almost always means concurrent modification. The enumerator’s own code is straightforward — there’s no realistic way for it to compute a bad index on its own. Something outside the enumerator changed the list while it was looking away.

If your stack trace shows that frame, stop looking for off-by-one errors in your LINQ predicate. Start looking for which other thread is writing to the same list.

Fixing it

The fix depends on the access pattern. In rough order of preference:

Use the right collection type. System.Collections.Concurrent has thread-safe equivalents tuned for different shapes of access:

  • ConcurrentBag<T> — many writers, occasional bulk drain. No keyed lookup or removal by item.
  • ConcurrentQueue<T> / ConcurrentStack<T> — FIFO / LIFO producer-consumer pipelines.
  • ConcurrentDictionary<TKey, TValue> — by far the most generally useful. Supports thread-safe add, remove, lookup, and snapshot enumeration via .Keys and .Values. If you’re keying items by an ID, this is almost always what you want.

Crucially: .Values on a ConcurrentDictionary returns a snapshot, so iterating it is safe even if other threads are mutating the dictionary at the same time. No exceptions, no locks.

Lock around access. If you’re stuck with List<T> — maybe the API surface is fixed, or the contention is low enough that the overhead doesn’t matter — wrap every read and every write in a lock on a dedicated private object. Every read and every write. Missing one is enough to bring the bug back.

private static readonly object _lock = new object();
private static readonly List<Thing> _items = new List<Thing>();
public static List<Thing> GetActiveThings()
{
lock (_lock)
{
return _items.Where(t => t.IsActive).ToList();
}
}
public static void AddThing(Thing t)
{
lock (_lock) { _items.Add(t); }
}

The ToList() inside the lock is deliberate — it forces the enumeration to complete before the lock is released, so the returned list is a safe, isolated copy the caller can work with at leisure.

Snapshot, then iterate. A halfway measure for read-mostly workloads:

var snapshot = Volatile.Read(ref _items).ToArray();

…paired with copy-on-write semantics for the writers. This is the pattern behind ImmutableList<T> from System.Collections.Immutable, which is worth knowing about for scenarios with many readers and rare writers.

What I’d take away from this

Two things.

First: when you see IndexOutOfRangeException coming out of a LINQ chain on a List<T>, your first hypothesis should be “another thread is writing to this list,” not “my predicate has a bug.” The stack trace looks like a logic error and it almost never is.

Second: List<T> being non-thread-safe is one of those facts every .NET developer knows in the abstract and still trips over in practice, because the framework gives you no help at all until something explodes. There’s no ThrowIfShared mode, no Roslyn analyzer that flags static List<T> fields, no runtime check at write time. The only feedback you get is a confusing exception from deep inside an enumerator, possibly weeks after deployment.

The fix is almost always “use the right type from System.Collections.Concurrent.” It costs you nothing in code clarity and saves you from a class of bug that’s genuinely painful to track down once it’s loose in production.