medusajs/medusa · error · MedusaError

Cannot revert a permanent failed transaction.

Error message

Cannot revert a permanent failed transaction.

What it means

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.

Source

Thrown at packages/core/orchestration/src/transaction/transaction-orchestrator.ts:1473

  /**
   * Cancel and revert a transaction compensating all its executed steps. It can be an ongoing transaction or a completed one
   * @param transaction - The transaction to be reverted
   */
  public async cancelTransaction(
    transaction: DistributedTransactionType,
    options?: { preventExecuteNext?: boolean }
  ): Promise<void> {
    if (transaction.modelId !== this.id) {
      throw new MedusaError(
        MedusaError.Types.NOT_ALLOWED,
        `TransactionModel "${transaction.modelId}" cannot be orchestrated by "${this.id}" model.`
      )
    }

    const flow = transaction.getFlow()
    if (flow.state === TransactionState.FAILED) {
      throw new MedusaError(
        MedusaError.Types.NOT_ALLOWED,
        `Cannot revert a permanent failed transaction.`
      )
    }

    if (
      flow.state === TransactionState.COMPENSATING ||
      flow.state === TransactionState.WAITING_TO_COMPENSATE
    ) {
      throw new MedusaError(
        MedusaError.Types.NOT_ALLOWED,
        `Cannot revert a transaction that is already compensating.`
      )
    }

    flow.state = TransactionState.WAITING_TO_COMPENSATE
    flow.cancelledAt = Date.now()

View on GitHub (pinned to 5e06e544a2)

Solutions

  1. Check transaction.getFlow().state before cancelling and surface 'already permanently failed' to the user
  2. Fetch fresh transaction state right before cancelling to avoid stale-state races
  3. If you need cleanup after permanent failure, handle it via compensation/failure handlers rather than cancel

Example fix

// before
await orchestrator.cancelTransaction(tx)

// after
if (tx.getFlow().state === "failed") {
  return { alreadyFailed: true }
}
await orchestrator.cancelTransaction(tx)
Defensive patterns

Strategy: validation

Validate before calling

const { state } = tx.getFlow()
if (state === 'failed') {
  // nothing to cancel; report to caller
  return { alreadyFailed: true }
}

Try / catch

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 }

Prevention

When it happens

Trigger: 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).

Common situations: 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.

Related errors


AI-assisted analysis of medusajs/medusa@5e06e544a2 (2026-08-27). Data as JSON: /api/errors/51a69a459f671d56. Report an issue: GitHub.