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
- 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
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
- Always fetch fresh flow state right before cancel/revert
- Treat permanent failure as a terminal state in your control flow
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
- Incorrect action type.
- Cannot skip a step when status is ${step.getStates().status}
- Cannot retry step when status is ${step.getStates().status}
- Method 'clearTransactionTimeout' not implemented.
- Method 'scheduleStepTimeout' not implemented.
AI-assisted analysis of medusajs/medusa@5e06e544a2 (2026-08-27).
Data as JSON: /api/errors/51a69a459f671d56.
Report an issue: GitHub.