medusajs/medusa · error · MedusaError

Cannot revert a transaction that is already compensating.

Error message

Cannot revert a transaction that is already compensating.

What it means

A transaction whose flow is already in COMPENSATING or WAITING_TO_COMPENSATE state cannot be asked to revert again — compensation is already underway. The orchestrator throws NOT_ALLOWED to prevent double-triggering rollback.

Source

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

      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()

    await transaction.saveCheckpoint()

    if (options?.preventExecuteNext) {
      return
    }

    await this.executeNext(transaction)
  }

  private parseFlowOptions() {

View on GitHub (pinned to 5e06e544a2)

Solutions

  1. Make cancel idempotent: check flow.state before calling and treat compensating as 'already in progress'
  2. Guard the endpoint with a lock or dedupe on transactionId
  3. Return 200/accepted when already compensating instead of erroring

Example fix

// before
app.post('/cancel/:id', async () => {
  await orchestrator.cancelTransaction(tx)
})

// after
const state = tx.getFlow().state
if (state === 'compensating' || state === 'waiting_to_compensate') {
  return { status: 'already-compensating' }
}
await orchestrator.cancelTransaction(tx)
Defensive patterns

Strategy: validation

Validate before calling

const state = tx.getFlow().state
if (state !== 'compensating' && state !== 'waiting_to_compensate') {
  await orchestrator.cancelTransaction(tx)
}

Type guard

const isCompensating = (tx) =>
  ['compensating', 'waiting_to_compensate'].includes(tx.getFlow().state)

Try / catch

try { await orchestrator.cancelTransaction(tx) } catch (e) { if (e.type === 'not_allowed' && /already compensating/.test(e.message)) return { status: 'already-compensating' } throw e }

Prevention

When it happens

Trigger: Calling cancelTransaction/revert twice (e.g. double-click, duplicated webhook), or calling it after an earlier call already moved the flow to WAITING_TO_COMPENSATE but before compensation finished.

Common situations: Non-idempotent cancel endpoints; concurrent cancel requests racing each other; monitoring tooling that automatically retries cancellation on timeout.

Related errors


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