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
- Return a cleanup function from the source: `return () => clearInterval(id)`
- If you have a Subscription object, that's fine — it has `.unsubscribe`; otherwise wrap the value: `return () => cleanupThing.close()`
- Fix concise arrow bodies that accidentally return values: use a block body `{ ...; }` when no return is intended
- 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
- Return () => {...} from source functions for teardown, or nothing at all
- Avoid concise arrow bodies that accidentally return values: use { } blocks
- A returned Subscription (object with .unsubscribe) is valid; other objects are not
- Test unsubscribe/teardown paths in unit tests to catch bad return values early
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
- Source must be a Function: ${String(source)}
- Observer must be an Object with callbacks: ${String(observer
- RelayObservable: Expected pollInterval to be positive, got:
- BabelPluginRelay: Expected plugin context to include "types"
- BabelPluginRelay: Expected exactly one definition per graphq
AI-assisted analysis of facebook/relay@668b1b85e0 (2026-09-02).
Data as JSON: /api/errors/96cfed2ed7009072.
Report an issue: GitHub.