{"record":{"id":"96cfed2ed7009072","repo":"facebook/relay","slug":"returned-cleanup-function-which-cannot-be-called","errorCode":null,"errorMessage":"Returned cleanup function which cannot be called: ${String(cleanup)}","messagePattern":"Returned cleanup function which cannot be called: (.+?)","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"packages/relay-runtime/network/RelayObservable.js","lineNumber":616,"sourceCode":"      }\n    },\n  });\n\n  // If anything goes wrong during observing the source, handle the error.\n  try {\n    cleanup = source(sink);\n  } catch (error) {\n    sink.error(error, true /* isUncaughtThrownError */);\n  }\n\n  if (__DEV__) {\n    // Early runtime errors for ill-formed returned cleanup.\n    if (\n      cleanup !== undefined &&\n      typeof cleanup !== 'function' &&\n      (!cleanup || typeof cleanup.unsubscribe !== 'function')\n    ) {\n      throw new Error(\n        'Returned cleanup function which cannot be called: ' + String(cleanup),\n      );\n    }\n  }\n\n  // If closed before the source function existed, cleanup now.\n  if (closed) {\n    doCleanup();\n  }\n\n  return subscription;\n}\n\nfunction swallowError(_error: Error, _isUncaughtThrownError: boolean): void {\n  // do nothing.\n}\n\nif (__DEV__) {","sourceCodeStart":598,"sourceCodeEnd":634,"githubUrl":"https://github.com/facebook/relay/blob/668b1b85e06261aa3b58dabfc51f8b5524a70955/packages/relay-runtime/network/RelayObservable.js#L598-L634","documentation":"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.","triggerScenarios":"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.","commonSituations":"`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.","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; })`"],"exampleFix":"// before\nRelayObservable.create(() => setInterval(tick, 1000)); // returns an id\n// after\nRelayObservable.create(() => {\n  const id = setInterval(tick, 1000);\n  return () => clearInterval(id);\n});","handlingStrategy":"validation","validationCode":"function safeCreate(sourceFn) {\n  return RelayObservable.create(sink => {\n    const cleanup = sourceFn(sink);\n    if (cleanup !== undefined && typeof cleanup !== 'function' && !(cleanup && typeof cleanup.unsubscribe === 'function')) {\n      throw new TypeError('Source must return undefined, a function, or a subscription');\n    }\n    return cleanup;\n  });\n}","typeGuard":"function isValidCleanup(c: unknown): c is undefined | (() => void) | {unsubscribe: () => void} {\n  return c === undefined || typeof c === 'function' || (typeof c === 'object' && c !== null && typeof (c as any).unsubscribe === 'function');\n}","tryCatchPattern":"try {\n  const sub = RelayObservable.create(source).subscribe(observer);\n} catch (e) {\n  if (e.message.startsWith('Returned cleanup function which cannot be called')) {\n    throw new TypeError('Your source function must return a cleanup function or subscription', {cause: e});\n  }\n  throw e;\n}","preventionTips":["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"],"tags":["relay","observable","teardown","dev-mode"],"backgroundTag":"invalid-observable-source","analyzedSha":"668b1b85e06261aa3b58dabfc51f8b5524a70955","analyzedAt":"2026-09-02T19:57:20.783Z","contentChangedAt":null,"schemaVersion":2},"datasetVersion":"2026-09-10T02:17:09.455Z"}