Docs Menu

Docs HomeDevelop ApplicationsMongoDB DriversNode.js

Transactions

On this page

  • Overview
  • Transaction APIs
  • Core API
  • Callback API
  • Transaction Settings
  • Examples
  • Sample Data
  • Core API Implementation
  • Callback API Implementation
  • Payment Transaction Result

Read this guide to learn how to perform transactions in MongoDB using the Node.js driver. A transaction is a unit of work, composed of a series of operations that you want either to succeed together, or fail together when one or more of the operations fail. This behavior is called atomicity. Atomicity is a property in which transactions composed of one or more operations occur all at once, such that no other client can observe them as separate operations, and that it leaves no changes if one of the operations fails.

Since all write operations on a single document in MongoDB are atomic, you may benefit most from transactions when you must make an atomic change that modifies multiple documents, which is called a multi-document transaction. Similar to write operations on a single document, multi-document transactions are ACID compliant, which means MongoDB guarantees the data involved in your transaction operations remains consistent, even if it encounters unexpected errors. Learn more from this MongoDB article on ACID transactions.

You can use the driver to perform multi-document transactions.

Note

To run multi-document transactions, you must use MongoDB version 4.0 or later.

For a detailed list of limitations, see the Transactions and Operations section in the server manual.

In MongoDB, multi-document transactions run within a client session. A client session is a grouping of related read or write operations that you want to ensure run sequentially. We recommend you reuse your client for multiple sessions and transactions instead of instantiating a new client each time.

When combined with majority read and write concerns, the driver can guarantee causal consistency between the operations. See the server manual guide on Client Sessions and Causal Consistency Guarantees for more information.

Learn more about how to use the driver to run multi-document transactions on MongoDB in the following sections of this guide:

To implement a transaction with the driver, you can use either the Core API or the Callback API. To use the Core API, you declare the start and commit points of the transaction. To use the Callback API, you pass a callback function that contains the logic you want to run as a transaction.

The Core API features methods to start, cancel, or commit a transaction. When you commit the transaction, you send a request to the server to make the changes from your operations atomically. When using this API, you must handle certain transaction errors returned by the server manually.

See TransientTransactionError and UnknownTransactionCommitResult for more information on these errors.

To start, cancel, or commit your transaction, you can call the corresponding method on the Session object:

  • startTransaction()

  • commitTransaction()

  • abortTransaction()

See the Core API example for a sample transaction implementation.

The Callback API enables you to run a transaction by passing a callback function that encapsulates the series of operations that make up the transaction. This API automatically handles certain transaction errors returned by the server.

See TransientTransactionError and UnknownTransactionCommitResult for more information on these errors.

To create a transaction, pass a callback function that encapsulates your transaction logic to the withTransaction() method on a Session object. The driver automatically retries the transaction when it encounters certain errors reported by the server.

See the Callback API example for a sample transaction implementation.

When you instantiate a transaction, you can specify the following options to set the default behavior for that transaction:

Setting
Description
readConcern

Specifies how to check for the consistency of the data that the read operations retrieve from replica sets.

See Read Concern in the server manual for more information.

writeConcern
Specifies conditions in which to acknowledge the write. See Write Concern in the server manual for more information.
readPreference
See Read Preference in the server manual for more information.
maxCommitTimeMS
Specifies the maximum amount of time to allow a commit action on a transaction to run in milliseconds.

If you do not provide values, the driver uses the client settings.

You can specify the transaction options in the Core API using code that resembles the following:

const transactionOptions = {
readPreference: 'primary',
readConcern: { level: 'local' },
writeConcern: { w: 'majority' },
maxCommitTimeMS: 1000
};
session.startTransaction(transactionOptions);

You can specify the transaction options in the Callback API using code that resembles the following:

const transactionOptions = {
readPreference: 'primary',
readConcern: { level: 'local' },
writeConcern: { w: 'majority' },
maxCommitTimeMS: 1000
};
await session.withTransaction(
async (session) => { /* your transaction here */ },
transactionOptions);

Consider a scenario in which a customer purchases items from your online store. To record the purchase, your application needs to update information related to inventory, the customer's orders, and register the order details. Suppose you organize the data updates as follows:

Collection
Operation
Description of the Change
orders
insert
Record the purchase information
customers
update
Append the order id to associate it with the customer
inventory
update
Subtract quantity of ordered items

A purchase can fail several ways such as if there's insufficient quantity of the item in inventory, if the order couldn't be completed, or if your payment system is offline.

If the payment fails, you can use a transaction to make sure that you avoid exposing any partial updates that might cause data consistency issues for other operations that depend on that data.

The code examples require the following sample data in the testdb database to run the multi-document payment transaction:

  • A document in the customers collection that describes a customer and their orders.

  • Documents in the inventory collection that each track quantity and description of an item.

The document in the customers collection in this example contains the following:

{ _id: 98765, orders: [] }

The documents in the inventory collection in this example contain the following:

[
{ name: "sunblock", sku: 5432, qty: 85 },
{ name: "beach towel", sku: 7865, qty: 41 }
]

The code examples also perform operations on the orders collection, but do not require any prior sample documents.

The code examples use the cart and payment variables to represent a sample list of items purchased and the order payment details as follows:

const cart = [
{ name: 'sunblock', sku: 5432, qty: 1, price: 5.19 },
{ name: 'beach towel', sku: 7865, qty: 2, price: 15.99 }
];
const payment = { customer: 98765, total: 37.17 };

Important

The examples in the following sections require that you create the collections outside of the transaction or that you are using MongoDB 4.4 or later. For more information on creating collections inside a transaction, see the Create Collections and Indexes in a Transaction server guide.

The code example in this section demonstrates how you can use the Core API to run the multi-document payment transaction in a session. This function shows how you can perform the following:

  1. Start a session

  2. Start a transaction, specifying transaction options

  3. Perform data operations in the same session

  4. Commit a transaction, or cancel it if the driver encounters an error

  5. End a session

1async function placeOrder(client, cart, payment) {
2 const transactionOptions = {
3 readConcern: { level: 'snapshot' },
4 writeConcern: { w: 'majority' },
5 readPreference: 'primary'
6 };
7
8 const session = client.startSession();
9 try {
10 session.startTransaction(transactionOptions);
11
12 const ordersCollection = client.db('testdb').collection('orders');
13 const orderResult = await ordersCollection.insertOne(
14 {
15 customer: payment.customer,
16 items: cart,
17 total: payment.total,
18 },
19 { session }
20 );
21
22 const inventoryCollection = client.db('testdb').collection('inventory');
23 for (let i=0; i<cart.length; i++) {
24 const item = cart[i];
25
26 // Cancel the transaction when you have insufficient inventory
27 const checkInventory = await inventoryCollection.findOne(
28 {
29 sku: item.sku,
30 qty: { $gte: item.qty }
31 },
32 { session }
33 )
34 if (checkInventory === null) {
35 throw new Error('Insufficient quantity or SKU not found.');
36 }
37
38 await inventoryCollection.updateOne(
39 { sku: item.sku },
40 { $inc: { 'qty': -item.qty }},
41 { session }
42 );
43 }
44
45 const customerCollection = client.db('testdb').collection('customers');
46 await customerCollection.updateOne(
47 { _id: payment.customer },
48 { $push: { orders: orderResult.insertedId }},
49 { session }
50 );
51 await session.commitTransaction();
52 console.log('Transaction successfully committed.');
53
54 } catch (error) {
55 if (error instanceof MongoError && error.hasErrorLabel('UnknownTransactionCommitResult')) {
56 // add your logic to retry or handle the error
57 }
58 else if (error instanceof MongoError && error.hasErrorLabel('TransientTransactionError')) {
59 // add your logic to retry or handle the error
60 } else {
61 console.log('An error occured in the transaction, performing a data rollback:' + error);
62 }
63 await session.abortTransaction();
64 } finally {
65 await session.endSession();
66 }
67}

Note that you must pass the session object to each CRUD operation that you want to run on that session.

The code and comments in the catch block demonstrate how you can identify the server transaction errors and where you can place your logic to handle them. Make sure to include the MongoError type from the driver in your code as shown in the following sample import statement:

const { MongoError, MongoClient } = require('mongodb');

See the Payment Transaction Result section to see what your collections should contain after you run the transaction.

The code examples in this section show how you can use the Callback API to run the multi-document payment transaction in a session.

The following code example shows how you can start the session and pass an anonymous callback function that contains the operations you want to run as a transaction.

1const transactionOptions = {
2 readPreference: 'primary',
3 readConcern: { level: 'local' },
4 writeConcern: { w: 'majority' },
5 maxCommitTimeMS: 1000
6};
7
8const session = client.startSession();
9try {
10 await session.withTransaction(
11 async (session) => { /* your transaction here */ },
12 transactionOptions);
13} catch(error) {
14 console.log('Encountered an error during the transaction: ' + error);
15} finally {
16 await session.endSession();
17}

Rather than pass an anonymous callback function, you can pass a named function that returns a Promise. See the following placeOrder() example callback function that you can pass to withTransaction() to run as a transaction:

1async function placeOrder(client, session, cart, payment) {
2 const ordersCollection = client.db('testdb').collection('orders');
3 const orderResult = await ordersCollection.insertOne(
4 {
5 customer: payment.customer,
6 items: cart,
7 total: payment.total,
8 },
9 { session }
10 );
11
12 const inventoryCollection = client.db('testdb').collection('inventory');
13 for (let i=0; i<cart.length; i++) {
14 const item = cart[i];
15
16 // Cancel the transaction when you have insufficient inventory
17 const checkInventory = await inventoryCollection.findOne(
18 {
19 sku: item.sku,
20 qty: { $gte: item.qty }
21 },
22 { session }
23 );
24
25 if (checkInventory === null) {
26 await session.abortTransaction();
27 console.error('Insufficient quantity or SKU not found.');
28 }
29
30 await inventoryCollection.updateOne(
31 { sku: item.sku },
32 { $inc: { 'qty': -item.qty }},
33 { session }
34 );
35 }
36
37 const customerCollection = client.db('testdb').collection('customers');
38 await customerCollection.updateOne(
39 { _id: payment.customer },
40 { $push: { orders: orderResult.insertedId }},
41 { session }
42 );
43}

See the Payment Transaction Result section to see what your collections should contain after you run the transaction.

If your application completes the payment transaction, your database should contain all the updates, and if an exception interrupted your transaction, none of the changes should exist in your database.

The customers collection should contain the customer document with an order id appended to the orders field:

{
"_id": 98765,
"orders": [
"61dc..."
]
}

The inventory collection should contain updated quantities for the items "sunblock" and "beach towel":

[
{
"_id": ...,
"name": "sunblock",
"sku": 5432,
"qty": 84
},
{
"_id": ...,
"name": "beach towel",
"sku": 7865,
"qty": 39
}
]

The orders collection should contain the order and payment information:

[
{
"_id": "...",
"customer": 98765,
"items": [
{
"name": "sunblock",
"sku": 5432,
"qty": 1,
"price": 5.19
},
{
"name": "beach towel",
"sku": 7865,
"qty": 2,
"price": 15.99
}
],
"total": 37.17
}
]
←  AggregationIndexes →