{"record":{"id":"d03358a86f678bbc","repo":"cube-js/cube","slug":"canceltoken-was-already-canceled","errorCode":null,"errorMessage":"CancelToken was already canceled","messagePattern":"CancelToken was already canceled","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"packages/cubejs-backend-shared/src/promises.ts","lineNumber":61,"sourceCode":"\n      resolve();\n    };\n  });\n  promise.cancel = cancel;\n\n  return promise;\n}\n\nclass CancelToken {\n  protected readonly deferred: (() => Promise<void> | void)[] = [];\n\n  protected readonly withQueue: CancelablePromiseCancel[] = [];\n\n  protected canceled = false;\n\n  public async cancel(): Promise<void> {\n    if (this.canceled) {\n      throw new Error('CancelToken was already canceled');\n    }\n\n    this.canceled = true;\n\n    if (this.deferred.length) {\n      await Promise.all(this.deferred.map(async (queued) => queued()));\n    }\n\n    if (this.withQueue.length) {\n      await Promise.all(\n        this.withQueue.map((cb) => cb())\n      );\n    }\n  }\n\n  public defer(fn: () => Promise<void> | void): void {\n    this.deferred.push(fn);\n  }","sourceCodeStart":43,"sourceCodeEnd":79,"githubUrl":"https://github.com/cube-js/cube/blob/7d981676b36392fec34088b9afab6bdcad40207c/packages/cubejs-backend-shared/src/promises.ts#L43-L79","documentation":"CancelToken.cancel() guards against double-cancellation: if the token's `canceled` flag is already true, calling cancel() again throws 'CancelToken was already canceled'. The token coordinates cancellation of deferred/queued CancelablePromises; canceling is a one-shot transition. Callers that issue cancel() more than once (e.g. from multiple event paths) hit this error.","triggerScenarios":"Calling token.cancel() twice — e.g. once from a request-abort handler and again on shutdown/timeout cleanup; or racing cancel() invocations that are not synchronized (the flag check is not atomic against concurrent async callers).","commonSituations":"Component unmount triggers cancel while a global timeout also cancels the same token; query orchestrator restart cancels tokens already canceled; duplicate event listeners each calling cancel().","solutions":["Check the token state before canceling, or track locally whether cancel was already issued on your side.","Wrap cancel() in try/catch and treat the 'already canceled' error as a no-op success.","Ensure a single owner/path is responsible for canceling a given token.","Serialize cancellation logic (idempotent wrapper storing a promise of the first cancel)."],"exampleFix":"// before\nawait token.cancel();\nawait token.cancel(); // throws\n// after\ntry { await token.cancel(); } catch (e) { /* already canceled: ignore */ }","handlingStrategy":"try-catch","validationCode":"// track cancellation state on your side before calling\nif (!alreadyCanceled) {\n  await token.cancel();\n  alreadyCanceled = true;\n}","typeGuard":"const isAlreadyCanceled = (e: unknown): boolean =>\n  e instanceof Error && e.message === 'CancelToken was already canceled';","tryCatchPattern":"try {\n  await token.cancel();\n} catch (e) {\n  if (!isAlreadyCanceled(e)) throw e; // double-cancel is treated as success\n}","preventionTips":["Assign a single owner responsible for canceling each token.","Make cancel paths idempotent (guard with a local flag or memoized promise).","Avoid multiple listeners each calling cancel on the same token.","In teardown code, always swallow the 'already canceled' error deliberately, not blindly."],"tags":["cancellation","async","state"],"backgroundTag":"double-cancel","analyzedSha":"7d981676b36392fec34088b9afab6bdcad40207c","analyzedAt":"2026-09-02T03:45:10.400Z","contentChangedAt":null,"schemaVersion":2},"datasetVersion":"2026-09-08T15:18:49.778Z"}