Transactions
On this page
Overview
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:
Transaction APIs
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.
Core API
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.
Callback API
Important
The Callback API is deprecated in Node.js driver version 4.10 and will be removed in a future release. We recommend that you use the Core API.
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.
Transaction Settings
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);
Examples
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.
Sample 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.
Core API Implementation
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:
Start a session
Start a transaction, specifying transaction options
Perform data operations in the same session
Commit a transaction, or cancel it if the driver encounters an error
End a session
1 async 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.
Callback API Implementation
Important
The Callback API is deprecated in Node.js driver version 4.10 and will be removed in a future release. We recommend that you use the Core API.
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.
1 const transactionOptions = { 2 readPreference: 'primary', 3 readConcern: { level: 'local' }, 4 writeConcern: { w: 'majority' }, 5 maxCommitTimeMS: 1000 6 }; 7 8 const session = client.startSession(); 9 try { 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:
1 async 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.
Payment Transaction Result
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 } ]