trpc/trpc · error · Error
Expected an async iterable
Error message
Expected an async iterable
What it means
The local (in-process) link handles subscriptions by awaiting the procedure result and iterating it. After `runProcedure(...)` it asserts `isAsyncIterable(result)`; the subscription contract requires an AsyncIterable/AsyncGenerator that yields values over time. A non-iterable return (plain value, Promise, array, Observable object) violates the contract and throws.
Source
Thrown at packages/client/src/links/localLink.ts:195
});
let lastEventId: string | undefined = undefined;
using _finally = makeResource({}, async () => {
observer.complete();
connectionState.next({
type: 'state',
state: 'idle',
error: null,
});
connectionSub.unsubscribe();
});
while (true) {
const result = await runProcedure(
inputWithTrackedEventId(op.input, lastEventId),
);
if (!isAsyncIterable(result)) {
throw new Error('Expected an async iterable');
}
await using iterator = iteratorResource(result);
observer.next({
result: {
type: 'started',
},
});
connectionState.next({
type: 'state',
state: 'pending',
error: null,
});
// Use a while loop to handle errors and reconnects
while (true) {
let res;
try {View on GitHub (pinned to acff82332d)
Solutions
- Return an async iterable from the subscription: an `async function*` or an object implementing `Symbol.asyncIterator`.
- Convert an existing Observable via the library's `observableToAsyncIterable` helper (or rxjs `firstValueFrom` loop).
- For event sources, wrap with an async generator that yields on each event.
Example fix
// before
subscription: t.procedure.subscription(() => someObservable) // throws
// after
subscription: t.procedure.subscription(async function* () {
for await (const evt of eventStream) yield evt;
}) Defensive patterns
Strategy: type-guard
Validate before calling
const isAsyncIterable = (v: unknown): v is AsyncIterable<unknown> =>
v != null && typeof (v as AsyncIterable<unknown>)[Symbol.asyncIterator] === 'function';
const result = await runProcedure(input);
if (!isAsyncIterable(result)) throw new TypeError('Subscription must return an async iterable'); Type guard
const isAsyncIterable = <T>(v: unknown): v is AsyncIterable<T> => v != null && typeof (v as AsyncIterable<T>)[Symbol.asyncIterator] === 'function';
Try / catch
try {
for await (const v of subscription) handle(v);
} catch (e) {
if (e instanceof Error && e.message === 'Expected an async iterable') {
// convert the resolver to an async generator
}
throw e;
} Prevention
- Always return an `async function*` from subscription resolvers.
- Unit-test subscription resolvers for AsyncIterable compliance.
When it happens
Trigger: Defining a `.subscription(...)` whose resolver returns a plain value, a `Promise`, a raw Observable, or anything that isn't an async iterable, then invoking it through a local caller (server-to-server, RSC, or `createTRPCClient` with a local link).
Common situations: Pre-v11 subscriptions that returned `observable(...)` not converted to async iterables, or returning `pubsub.asyncIterator()` from a lib whose API changed.
Related errors
- Subscriptions are unsupported by `httpLink` - use `httpSubsc
- Subscriptions are unsupported by `httpBatchStreamLink` - use
- Subscriptions are unsupported by `httpLink` - use `httpSubsc
- httpSubscriptionLink only supports subscriptions
- NOT_IMPLEMENTED
AI-assisted analysis of trpc/trpc@acff82332d (2026-08-12).
Data as JSON: /api/errors/b8c125acc442f224.
Report an issue: GitHub.