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
- Make cancel idempotent: check flow.state before calling and treat compensating as 'already in progress'
- Guard the endpoint with a lock or dedupe on transactionId
- 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
- Make cancel endpoints idempotent and dedupe concurrent cancel requests by transaction id
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
- Method 'clearTransactionTimeout' not implemented.
- Method 'scheduleStepTimeout' not implemented.
- Method 'clearStepTimeout' not implemented.
- Method 'clearExpiredExecutions' not implemented.
- Unable to serialize context object. Please make sure the wor
AI-assisted analysis of medusajs/medusa@5e06e544a2 (2026-08-27).
Data as JSON: /api/errors/e0b85d7b0941657c.
Report an issue: GitHub.