mongodb/node-mongodb-native · error · MongoTransactionError

Cannot call abortTransaction twice

Error message

Cannot call abortTransaction twice

What it means

Thrown by ClientSession.abortTransaction() when the transaction state is already TRANSACTION_ABORTED. Double-abort is rejected by the driver because the first abort already transitioned the state machine out of an abortable state. It is a MongoTransactionError.

Source

Thrown at src/sessions.ts:587

   *
   * @param options - Optional options, can be used to override `defaultTimeoutMS`.
   */
  async abortTransaction(options?: { timeoutMS?: number }): Promise<void>;
  /** @internal */
  async abortTransaction(options?: { timeoutMS?: number; throwTimeout?: true }): Promise<void>;
  async abortTransaction(options?: { timeoutMS?: number; throwTimeout?: true }): Promise<void> {
    if (this.transaction.state === TxnState.NO_TRANSACTION) {
      throw new MongoTransactionError('No transaction started');
    }

    if (this.transaction.state === TxnState.STARTING_TRANSACTION) {
      // the transaction was never started, we can safely exit here
      this.transaction.transition(TxnState.TRANSACTION_ABORTED);
      return;
    }

    if (this.transaction.state === TxnState.TRANSACTION_ABORTED) {
      throw new MongoTransactionError('Cannot call abortTransaction twice');
    }

    if (
      this.transaction.state === TxnState.TRANSACTION_COMMITTED ||
      this.transaction.state === TxnState.TRANSACTION_COMMITTED_EMPTY
    ) {
      throw new MongoTransactionError(
        'Cannot call abortTransaction after calling commitTransaction'
      );
    }

    const command: {
      abortTransaction: 1;
      writeConcern?: WriteConcernOptions;
      recoveryToken?: Document;
    } = { abortTransaction: 1 };

    const timeoutMS =

View on GitHub (pinned to 3366c21a63)

Solutions

  1. Guard with active-state check: if (session.inTransaction()) await session.abortTransaction(). Note inTransaction() returns false once aborted, so this naturally prevents double-abort.
  2. Centralize abort in one place (either catch or finally, not both).
  3. Switch to withTransaction() to avoid manual abort management.

Example fix

// before
try { session.startTransaction(); await coll.insertOne(doc, { session }); }
catch (e) { await session.abortTransaction(); throw e; }
finally { await session.abortTransaction(); } // throws: already aborted

// after
try { session.startTransaction(); await coll.insertOne(doc, { session }); }
catch (e) { throw e; }
finally { if (session.inTransaction()) await session.abortTransaction(); }
Defensive patterns

Strategy: validation

Validate before calling

if (session.inTransaction()) {
  await session.abortTransaction(); // inTransaction() is false once aborted
}

Type guard

function isAbortable(session: ClientSession): boolean {
  return session.transaction.isActive;
}

Try / catch

try {
  await session.abortTransaction();
} catch (e) {
  if (e instanceof MongoTransactionError && /abortTransaction twice/.test(e.message)) {
    // already aborted; safe to ignore
  } else throw e;
}

Prevention

When it happens

Trigger: A catch block calls abortTransaction() and then a finally block calls abortTransaction() again; explicit abort followed by another explicit abort.

Common situations: Boilerplate try/catch/finally where both catch and finally abort; nested functions where each layer defensively aborts.

Related errors


AI-assisted analysis of mongodb/node-mongodb-native@3366c21a63 (2026-08-04). Data as JSON: /data/errors/9653dc156bfc3c7a.json. Report an issue: GitHub.