Cysharp/UniTask · error · InvalidOperationException

Enumerator is already running, does not allow call GetAsyncE

Error message

Enumerator is already running, does not allow call GetAsyncEnumerator twice.

What it means

Thrown by SingleConsumerUnboundedChannel's ReadAllAsyncEnumerable when GetAsyncEnumerator is called a second time. The enumerable tracks a boolean 'running' flag set on first enumeration; a second call is rejected because the channel is designed for single-consumer use. Multiple concurrent enumerators would corrupt the shared queue.

Source

Thrown at src/UniTask/Assets/Plugins/UniTask/Runtime/Channel.cs:388

                CancellationToken cancellationToken2;
                CancellationTokenRegistration cancellationTokenRegistration1;
                CancellationTokenRegistration cancellationTokenRegistration2;

                T current;
                bool cacheValue;
                bool running;

                public ReadAllAsyncEnumerable(SingleConsumerUnboundedChannelReader parent, CancellationToken cancellationToken)
                {
                    this.parent = parent;
                    this.cancellationToken1 = cancellationToken;
                }

                public IUniTaskAsyncEnumerator<T> GetAsyncEnumerator(CancellationToken cancellationToken = default)
                {
                    if (running)
                    {
                        throw new InvalidOperationException("Enumerator is already running, does not allow call GetAsyncEnumerator twice.");
                    }

                    if (this.cancellationToken1 != cancellationToken)
                    {
                        this.cancellationToken2 = cancellationToken;
                    }

                    if (this.cancellationToken1.CanBeCanceled)
                    {
                        this.cancellationTokenRegistration1 =  this.cancellationToken1.RegisterWithoutCaptureExecutionContext(CancellationCallback1Delegate, this);
                    }

                    if (this.cancellationToken2.CanBeCanceled)
                    {
                        this.cancellationTokenRegistration2 = this.cancellationToken2.RegisterWithoutCaptureExecutionContext(CancellationCallback2Delegate, this);
                    }

                    running = true;

View on GitHub (pinned to ceac8d6946)

Solutions

  1. Use only one consumer per SingleConsumerUnboundedChannel — if multiple consumers are needed, create separate channels or implement a broadcast layer.
  2. Create a new channel via Channel.CreateSingleConsumerUnbounded<T>() for each independent consumer.
  3. If you need multi-consumer fan-out, subscribe each consumer to its own channel and forward items manually.

Example fix

// before (throws on second consumer)
var enumerable = channel.Reader.ReadAllAsync();
await foreach (var x in enumerable) { }
await foreach (var y in enumerable) { } // throws

// after
var ch1 = Channel.CreateSingleConsumerUnbounded<int>();
var ch2 = Channel.CreateSingleConsumerUnbounded<int>();
// producer writes to both
await foreach (var x in ch1.Reader.ReadAllAsync()) { }
await foreach (var y in ch2.Reader.ReadAllAsync()) { }
Defensive patterns

Strategy: validation

Validate before calling

// Ensure only one consumer per channel
// Before enumerating, verify the channel is a fresh single-consumer channel
// and you are the sole consumer.
// There is no public IsEnumerating flag — design your code so each
// SingleConsumerUnboundedChannel has exactly one ReadAllAsync caller.

Try / catch

try
{
    await foreach (var item in channel.Reader.ReadAllAsync()) { /* ... */ }
}
catch (InvalidOperationException ex) when (ex.Message.Contains("GetAsyncEnumerator twice"))
{
    // A second consumer attempted enumeration; redesign for single consumer
}

Prevention

When it happens

Trigger: Calling 'await foreach' twice on the same IUniTaskAsyncEnumerable returned by reader.ReadAllAsync(). Or manually invoking GetAsyncEnumerator() twice on that enumerable. Or sharing the same single-consumer channel reader between two concurrent consumer tasks.

Common situations: Fan-out pattern incorrectly applied to a SingleConsumerUnboundedChannel. A test or utility that re-enumerates the same source. Code that breaks out of an await foreach loop and then tries to enumerate again.

Related errors


AI-assisted analysis of Cysharp/UniTask@ceac8d6946 (2026-08-13). Data as JSON: /api/errors/7749128bea4c031b. Report an issue: GitHub.