Cysharp/UniTask · error · ChannelClosedException
Channel is already closed.
Error message
Channel is already closed.
What it means
Thrown by ReadAsyncCore when a consumer calls ReadAsync on a channel that is already completed and empty. After WaitToReadAsync returns false (signalling the channel is done) or returns true but TryRead finds nothing (items were drained between signal and read), the method has no data to return and throws ChannelClosedException. This mirrors the semantics of System.Threading.Channels.
Source
Thrown at src/UniTask/Assets/Plugins/UniTask/Runtime/Channel.cs:55
if (this.TryRead(out var item))
{
return UniTask.FromResult(item);
}
return ReadAsyncCore(cancellationToken);
}
async UniTask<T> ReadAsyncCore(CancellationToken cancellationToken = default(CancellationToken))
{
if (await WaitToReadAsync(cancellationToken))
{
if (TryRead(out var item))
{
return item;
}
}
throw new ChannelClosedException();
}
public abstract IUniTaskAsyncEnumerable<T> ReadAllAsync(CancellationToken cancellationToken = default(CancellationToken));
}
public abstract class ChannelWriter<T>
{
public abstract bool TryWrite(T item);
public abstract bool TryComplete(Exception error = null);
public void Complete(Exception error = null)
{
if (!TryComplete(error))
{
throw new ChannelClosedException();
}
}
}View on GitHub (pinned to ceac8d6946)
Solutions
- Use ReadAllAsync (await foreach) instead of manual ReadAsync loops — it gracefully ends enumeration when the channel completes.
- Before calling ReadAsync, check reader.Completion.IsCompleted and reader.TryRead to avoid hitting the closed-channel path.
- Ensure the producer does not call Complete() until the consumer has finished consuming, or coordinate via a sentinel value.
- Wrap ReadAsync in try-catch for ChannelClosedException if completion is expected mid-read.
Example fix
// before
var item = await channel.Reader.ReadAsync(); // throws if channel completed
// after
if (channel.Reader.TryRead(out var item))
{
// use item
}
else if (await channel.Reader.Completion is { IsFaulted: false })
{
// channel completed normally, nothing to read
} Defensive patterns
Strategy: try-catch
Validate before calling
// Check completion state before ReadAsync
if (channel.Reader.Completion.IsCompleted)
{
// Channel is closed; do not call ReadAsync
return;
}
if (channel.Reader.TryRead(out var item))
{
// got item safely
} Try / catch
try
{
var item = await channel.Reader.ReadAsync(cancellationToken);
}
catch (ChannelClosedException)
{
// Channel was completed; expected during shutdown
break;
} Prevention
- Prefer ReadAllAsync (await foreach) over manual ReadAsync loops — it terminates cleanly on channel completion.
- Coordinate producer/consumer lifecycle so the channel is not completed while a consumer is mid-read.
- Use a sentinel item to signal end-of-stream rather than relying on completion exceptions.
When it happens
Trigger: Calling ChannelReader<T>.ReadAsync() on a channel whose writer has already called Complete() and whose queue is empty. Also fires in a race where the channel completes between WaitToReadAsync signalling data-available and the subsequent TryRead.
Common situations: A producer calls Writer.Complete() while a consumer is mid-await on ReadAsync. A consumer reads items in a loop and the final ReadAsync hits the closed channel. Shutdown logic completes the channel before the consumer drains remaining items.
Related errors
- Enumerator is already running, does not allow call GetAsyncE
- continuation is already registered.
- Attempting to use an invalid operation handle
- Not yet completed.
- AsyncSubject is not completed yet
AI-assisted analysis of Cysharp/UniTask@ceac8d6946 (2026-08-13).
Data as JSON: /api/errors/e4404f3e65641a11.
Report an issue: GitHub.