facebook/relay · error

Returned cleanup function which cannot be called: ${String(c

Error message

Returned cleanup function which cannot be called: ${String(cleanup)}

What it means

When a source function returns a cleanup value, it must be either undefined, a function to call on teardown, or an object with an `unsubscribe` method (a Subscription). This dev-mode check throws when the source returns something else (a number, string, plain object, etc.), indicating the source was written incorrectly.

Source

Thrown at packages/relay-runtime/network/RelayObservable.js:616

      }
    },
  });

  // If anything goes wrong during observing the source, handle the error.
  try {
    cleanup = source(sink);
  } catch (error) {
    sink.error(error, true /* isUncaughtThrownError */);
  }

  if (__DEV__) {
    // Early runtime errors for ill-formed returned cleanup.
    if (
      cleanup !== undefined &&
      typeof cleanup !== 'function' &&
      (!cleanup || typeof cleanup.unsubscribe !== 'function')
    ) {
      throw new Error(
        'Returned cleanup function which cannot be called: ' + String(cleanup),
      );
    }
  }

  // If closed before the source function existed, cleanup now.
  if (closed) {
    doCleanup();
  }

  return subscription;
}

function swallowError(_error: Error, _isUncaughtThrownError: boolean): void {
  // do nothing.
}

if (__DEV__) {

View on GitHub (pinned to 668b1b85e0)

Solutions

  1. Return a cleanup function from the source: `return () => clearInterval(id)`
  2. If you have a Subscription object, that's fine — it has `.unsubscribe`; otherwise wrap the value: `return () => cleanupThing.close()`
  3. Fix concise arrow bodies that accidentally return values: use a block body `{ ...; }` when no return is intended
  4. If you cannot change the source, wrap it: `create(sink => { const r = badSource(sink); return typeof r === 'function' ? r : undefined; })`

Example fix

// before
RelayObservable.create(() => setInterval(tick, 1000)); // returns an id
// after
RelayObservable.create(() => {
  const id = setInterval(tick, 1000);
  return () => clearInterval(id);
});
Defensive patterns

Strategy: validation

Validate before calling

function safeCreate(sourceFn) {
  return RelayObservable.create(sink => {
    const cleanup = sourceFn(sink);
    if (cleanup !== undefined && typeof cleanup !== 'function' && !(cleanup && typeof cleanup.unsubscribe === 'function')) {
      throw new TypeError('Source must return undefined, a function, or a subscription');
    }
    return cleanup;
  });
}

Type guard

function isValidCleanup(c: unknown): c is undefined | (() => void) | {unsubscribe: () => void} {
  return c === undefined || typeof c === 'function' || (typeof c === 'object' && c !== null && typeof (c as any).unsubscribe === 'function');
}

Try / catch

try {
  const sub = RelayObservable.create(source).subscribe(observer);
} catch (e) {
  if (e.message.startsWith('Returned cleanup function which cannot be called')) {
    throw new TypeError('Your source function must return a cleanup function or subscription', {cause: e});
  }
  throw e;
}

Prevention

When it happens

Trigger: A source passed to `RelayObservable.create(source)` that returns a non-function, non-subscription value — e.g. returning a promise without `.then`, returning `this` mistakenly, or an arrow function with a concise body returning an unintended value.

Common situations: `create(() => doCleanup())` where doCleanup returns a boolean/promise result; concise arrow `create(() => intervalId)` returning a timer id instead of a clear function; confusion with RxJS teardown conventions.

Related errors


AI-assisted analysis of facebook/relay@668b1b85e0 (2026-09-02). Data as JSON: /api/errors/96cfed2ed7009072. Report an issue: GitHub.