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

  1. Return an async iterable from the subscription: an `async function*` or an object implementing `Symbol.asyncIterator`.
  2. Convert an existing Observable via the library's `observableToAsyncIterable` helper (or rxjs `firstValueFrom` loop).
  3. 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

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


AI-assisted analysis of trpc/trpc@acff82332d (2026-08-12). Data as JSON: /api/errors/b8c125acc442f224. Report an issue: GitHub.