ReactiveX/rxjs · error · TypeError

Iterator return() must return an Object

Error message

Iterator return() must return an Object

What it means

During teardown/cancellation, the polyfill closes a sync iterator by calling its optional return() method (closeSyncIterator → closeIterator). The iterator protocol requires return() to respond with an object; if it returns a primitive or undefined, this TypeError is thrown while unsubscribing from an Observable created from an iterable.

Source

Thrown at packages/observable-polyfill/src/index.ts:385

    const iterator = asyncIteratorMethod.call(value);
    if (!isObject(iterator)) {
      throw new TypeError('Symbol.asyncIterator must return an object');
    }
    return { iterator };
  }

  return { iterator: getSyncIteratorRecord(value).iterator };
}

function closeSyncIterator(record: SyncIteratorRecord<unknown>, reason: unknown): void {
  const returnMethod = getMethod(record.iterator, 'return');
  if (!returnMethod) {
    return;
  }

  const result = returnMethod.call(record.iterator, reason);
  if (!isObject(result)) {
    throw new TypeError('Iterator return() must return an Object');
  }
}

function closeAsyncIterator(record: AsyncIteratorRecord, reason: unknown): void {
  let result: unknown;
  try {
    const returnMethod = getMethod(record.iterator, 'return');
    if (!returnMethod) {
      return;
    }
    result = returnMethod.call(record.iterator, reason);
  } catch (error) {
    globalThis.queueMicrotask(() => reportUnhandledRejection(error));
    return;
  }

  void Promise.resolve(result).then(
    (returnResult) => {

View on GitHub (pinned to 54796b38a5)

Solutions

  1. In the iterator's return(reason) method, end with return { done: true }; (returning any object).
  2. If delegating: return inner.return?.() ?? { done: true };
  3. Remove return() entirely if no cleanup is needed — its absence is legal.
  4. Write a test that subscribes then unsubscribes mid-iteration to exercise the return() path.

Example fix

// before
const it = {
  next: () => ({ done: false, value: 1 }),
  return() { cleanup(); } // returns undefined
};
// after
const it = {
  next: () => ({ done: false, value: 1 }),
  return() { cleanup(); return { done: true }; }
};
Defensive patterns

Strategy: validation

Validate before calling

const it = iterable[Symbol.iterator]();
if (typeof (it as any).return === 'function') {
  const probe = { done: true } as const; // only test shape on a throwaway iterator in tests
}

Type guard

function hasValidReturn(it: Iterator<unknown>): boolean {
  return typeof (it as any).return !== 'function'; // absence is safe; presence must return objects
}

Try / catch

subscriber.addTeardown(() => { try { iterator.return?.(reason); } catch { /* ignore teardown errors */ } });

Prevention

When it happens

Trigger: Unsubscribing (or an error/early completion causing teardown) from Observable.from(iterable) where the iterable's iterator defines a return() that returns a non-object (commonly undefined).

Common situations: Hand-written iterators implementing return() for cleanup but forgetting to return an object; partially-implemented iterator mocks in tests; delegating iterators whose return() forwards a void cleanup function's result.

Related errors


AI-assisted analysis of ReactiveX/rxjs@54796b38a5 (2026-08-28). Data as JSON: /api/errors/f782ba9a0cfa5c4e. Report an issue: GitHub.