Firestore Transactions
A transaction in Firestore is a set of read and write operations that execute as a single unit. Either all operations succeed together, or none of them take effect. Transactions prevent data inconsistency when multiple users try to update the same data at the same time.
Why Transactions Matter
Imagine two customers both try to buy the last concert ticket at the same moment. Without a transaction:
Customer A reads stock: 1 ticket available Customer B reads stock: 1 ticket available Customer A writes: stock = 0, ticket sold to A Customer B writes: stock = 0, ticket sold to B Result: 2 tickets sold, stock shows 0, one customer gets nothing
With a transaction, Firestore detects the conflict and retries. Only one customer gets the ticket.
How Firestore Transactions Work
A Firestore transaction works in a loop:
1. Start transaction 2. Read the document(s) you need 3. Compute new values based on what you read 4. Write the new values 5. Firestore checks if the documents changed between step 2 and step 4 - If no change: commit the writes - If changed: roll back and retry from step 2
Basic Transaction Example
import { doc, runTransaction } from "firebase/firestore";
import { db } from "./firebase";
async function purchaseTicket(eventId, userId) {
const eventRef = doc(db, "events", eventId);
try {
await runTransaction(db, async (transaction) => {
const eventDoc = await transaction.get(eventRef);
if (!eventDoc.exists()) {
throw new Error("Event not found.");
}
const availableTickets = eventDoc.data().availableTickets;
if (availableTickets <= 0) {
throw new Error("No tickets left.");
}
// Deduct one ticket and record the purchase
transaction.update(eventRef, {
availableTickets: availableTickets - 1
});
const purchaseRef = doc(db, "purchases", userId + "_" + eventId);
transaction.set(purchaseRef, {
userId,
eventId,
purchasedAt: new Date()
});
});
console.log("Ticket purchased successfully.");
} catch (error) {
console.error("Purchase failed:", error.message);
}
}
Rules Inside Transactions
Transactions have specific requirements:
- All reads must happen before any writes in the transaction function
- The transaction function may run multiple times if conflicts occur — keep it free of side effects like sending emails
- Transactions time out after 30 seconds
- A single transaction can read and write up to 500 documents
Transferring Value Between Documents
Transactions work well for transferring values between documents — like moving currency from one wallet to another:
async function transferCoins(fromUserId, toUserId, amount) {
const fromRef = doc(db, "wallets", fromUserId);
const toRef = doc(db, "wallets", toUserId);
await runTransaction(db, async (transaction) => {
const fromDoc = await transaction.get(fromRef);
const toDoc = await transaction.get(toRef);
const fromBalance = fromDoc.data().coins;
const toBalance = toDoc.data().coins;
if (fromBalance < amount) {
throw new Error("Insufficient coins.");
}
transaction.update(fromRef, { coins: fromBalance - amount });
transaction.update(toRef, { coins: toBalance + amount });
});
console.log("Transfer complete.");
}
When to Use Transactions vs increment()
For simple counter increments where you don't need to read the current value before writing, use increment() instead of a transaction. It is atomic and simpler:
// Simple: just add 1 to likesCount
await updateDoc(doc(db, "posts", "post_001"), {
likesCount: increment(1)
});
// Complex: read value, apply business logic, write result
// Use a transaction
Use transactions when the new value depends on reading the current value AND applying conditional logic based on that value.
Key Takeaway
Firestore transactions guarantee that a group of reads and writes execute atomically — all succeed or all fail. They prevent data races when multiple users update shared data simultaneously. Always do all reads before any writes inside a transaction. Use increment() for simple counter updates and reserve transactions for operations that require reading a value, checking it, and writing a new value that depends on it.
