{"record":{"id":"51a69a459f671d56","repo":"medusajs/medusa","slug":"cannot-revert-a-permanent-failed-transaction","errorCode":null,"errorMessage":"Cannot revert a permanent failed transaction.","messagePattern":"Cannot revert a permanent failed transaction\\.","errorType":"exception","errorClass":"MedusaError","httpStatus":null,"severity":"error","filePath":"packages/core/orchestration/src/transaction/transaction-orchestrator.ts","lineNumber":1473,"sourceCode":"\n  /**\n   * Cancel and revert a transaction compensating all its executed steps. It can be an ongoing transaction or a completed one\n   * @param transaction - The transaction to be reverted\n   */\n  public async cancelTransaction(\n    transaction: DistributedTransactionType,\n    options?: { preventExecuteNext?: boolean }\n  ): Promise<void> {\n    if (transaction.modelId !== this.id) {\n      throw new MedusaError(\n        MedusaError.Types.NOT_ALLOWED,\n        `TransactionModel \"${transaction.modelId}\" cannot be orchestrated by \"${this.id}\" model.`\n      )\n    }\n\n    const flow = transaction.getFlow()\n    if (flow.state === TransactionState.FAILED) {\n      throw new MedusaError(\n        MedusaError.Types.NOT_ALLOWED,\n        `Cannot revert a permanent failed transaction.`\n      )\n    }\n\n    if (\n      flow.state === TransactionState.COMPENSATING ||\n      flow.state === TransactionState.WAITING_TO_COMPENSATE\n    ) {\n      throw new MedusaError(\n        MedusaError.Types.NOT_ALLOWED,\n        `Cannot revert a transaction that is already compensating.`\n      )\n    }\n\n    flow.state = TransactionState.WAITING_TO_COMPENSATE\n    flow.cancelledAt = Date.now()\n","sourceCodeStart":1455,"sourceCodeEnd":1491,"githubUrl":"https://github.com/medusajs/medusa/blob/5e06e544a296b9033f20f71f11c559f81a0e5739/packages/core/orchestration/src/transaction/transaction-orchestrator.ts#L1455-L1491","documentation":"cancelTransaction (which internally handles revert semantics) refuses to operate on a transaction whose flow state is FAILED — a permanent failure. Once the flow itself has failed (not just a step), there is nothing to cancel/revert and the engine throws NOT_ALLOWED.","triggerScenarios":"Calling cancelTransaction/revert on a DistributedTransaction whose getFlow().state === TransactionState.FAILED, i.e. after the workflow definitively failed (e.g. a step failed with maxRetries exceeded and no compensation possible or already completed failure handling).","commonSituations":"Retry/cancel UI logic that operates on stale transaction state fetched before the flow finished failing; race where the transaction fails while a cancel request is in flight.","solutions":["Check transaction.getFlow().state before cancelling and surface 'already permanently failed' to the user","Fetch fresh transaction state right before cancelling to avoid stale-state races","If you need cleanup after permanent failure, handle it via compensation/failure handlers rather than cancel"],"exampleFix":"// before\nawait orchestrator.cancelTransaction(tx)\n\n// after\nif (tx.getFlow().state === \"failed\") {\n  return { alreadyFailed: true }\n}\nawait orchestrator.cancelTransaction(tx)","handlingStrategy":"validation","validationCode":"const { state } = tx.getFlow()\nif (state === 'failed') {\n  // nothing to cancel; report to caller\n  return { alreadyFailed: true }\n}","typeGuard":null,"tryCatchPattern":"try { await orchestrator.cancelTransaction(tx) } catch (e) { if (e.type === 'not_allowed' && /permanent failed/.test(e.message)) return { ok: true, reason: 'already-failed' } throw e }","preventionTips":["Always fetch fresh flow state right before cancel/revert","Treat permanent failure as a terminal state in your control flow"],"tags":["orchestration","workflow","cancel","invalid-state"],"backgroundTag":"invalid-workflow-state-transition","analyzedSha":"5e06e544a296b9033f20f71f11c559f81a0e5739","analyzedAt":"2026-08-27T07:24:39.599Z","schemaVersion":2},"datasetVersion":"2026-08-27T08:17:20.692Z"}