To update a document in Firestore, use updateDoc() for partial field updates or setDoc() with merge: true for upsert behavior. The modular SDK v9+ provides atomic field modifiers like increment(), arrayUnion(), arrayRemove(), and serverTimestamp() that let you modify specific values without reading the document first. Always ensure security rules permit update operations for authenticated users.
Updating Documents in Firestore with the Modular SDK
Firestore supports two approaches to modifying existing documents: updateDoc() for partial updates (fails if the document does not exist) and setDoc() with merge: true for upsert behavior (creates the document if missing). This tutorial covers both methods, shows you how to use atomic modifiers for counters and arrays, explains nested field updates with dot notation, and includes security rules that validate update operations.
Prerequisites
- A Firebase project with Firestore database created
- Firebase JS SDK v9+ installed in your project
- At least one document in a Firestore collection to update
- Basic understanding of JavaScript or TypeScript
Step-by-step guide
Update specific fields with updateDoc()
Update specific fields with updateDoc()
The updateDoc() function modifies only the fields you specify, leaving all other fields unchanged. It requires a document reference and an object of field-value pairs. If the document does not exist, updateDoc() throws a 'not-found' error. Use this when you know the document already exists and you want to change specific fields.
1import { doc, updateDoc } from 'firebase/firestore';2import { db } from './firebase';34async function updateUserName(userId: string, newName: string) {5 const userRef = doc(db, 'users', userId);67 await updateDoc(userRef, {8 displayName: newName,9 updatedAt: new Date(),10 });1112 console.log('User name updated successfully');13}Expected result: The displayName and updatedAt fields are updated, and all other fields on the document remain unchanged.
Upsert with setDoc() and merge option
Upsert with setDoc() and merge option
Use setDoc() with { merge: true } when you want to update a document if it exists or create it if it does not. This is useful for idempotent operations where you do not want to check existence first. Without the merge option, setDoc() overwrites the entire document, deleting any fields not included in the new data.
1import { doc, setDoc } from 'firebase/firestore';23async function upsertUserProfile(userId: string, data: Record<string, any>) {4 const userRef = doc(db, 'users', userId);56 // Creates the document if it doesn't exist,7 // or merges fields if it does8 await setDoc(userRef, {9 ...data,10 lastSeen: new Date(),11 }, { merge: true });12}Expected result: The document is created with the provided data if new, or the specified fields are merged into the existing document.
Use atomic modifiers for counters and arrays
Use atomic modifiers for counters and arrays
Firestore provides special field values that perform atomic operations server-side. increment() adds or subtracts from a number field without reading the current value. arrayUnion() adds elements to an array only if they are not already present. arrayRemove() removes all instances of specified elements. serverTimestamp() writes the server's current time, ensuring consistency.
1import { doc, updateDoc, increment, arrayUnion, arrayRemove, serverTimestamp } from 'firebase/firestore';23// Increment a counter without reading current value4await updateDoc(doc(db, 'posts', 'post123'), {5 viewCount: increment(1),6 lastViewed: serverTimestamp(),7});89// Add a tag to an array (only if not already present)10await updateDoc(doc(db, 'posts', 'post123'), {11 tags: arrayUnion('firebase'),12});1314// Remove a tag from an array15await updateDoc(doc(db, 'posts', 'post123'), {16 tags: arrayRemove('deprecated'),17});Expected result: Fields are modified atomically on the server without needing to read the current document value first.
Update nested fields with dot notation
Update nested fields with dot notation
When a document has nested objects, use dot-separated field paths to update specific nested fields without overwriting the entire parent object. Without dot notation, passing a nested object to updateDoc() replaces the entire nested object, removing any sibling fields you did not include.
1import { doc, updateDoc } from 'firebase/firestore';23// Document structure: { address: { city: 'NYC', state: 'NY', zip: '10001' } }45// CORRECT: Update only the city, keep state and zip6await updateDoc(doc(db, 'users', 'user123'), {7 'address.city': 'Los Angeles',8 'address.state': 'CA',9});1011// WRONG: This replaces the entire address object, deleting zip12// await updateDoc(doc(db, 'users', 'user123'), {13// address: { city: 'Los Angeles', state: 'CA' },14// });Expected result: Only the specified nested fields are updated while sibling fields in the same nested object remain untouched.
Write security rules for update operations
Write security rules for update operations
Firestore security rules let you control who can update documents and which fields they can modify. The request.resource.data object contains the document as it would look after the update. Compare it with resource.data (the current document) to validate specific changes. You can restrict updates to the document owner and validate that certain fields are not modified.
1// firestore.rules2rules_version = '2';3service cloud.firestore {4 match /databases/{database}/documents {5 match /users/{userId} {6 // Only the user can update their own document7 allow update: if request.auth != null8 && request.auth.uid == userId9 // Prevent users from changing their role field10 && request.resource.data.role == resource.data.role;11 }1213 match /posts/{postId} {14 // Only the author can update their post15 allow update: if request.auth != null16 && request.auth.uid == resource.data.authorId;17 }18 }19}Expected result: Only authorized users can update documents, and protected fields like role cannot be changed by the client.
Handle update errors gracefully
Handle update errors gracefully
Firestore update operations can fail for several reasons: the document does not exist (updateDoc only), security rules deny the operation, or the client is offline without persistence enabled. Always wrap updates in try-catch blocks and provide meaningful error messages to the user.
1import { doc, updateDoc, FirestoreError } from 'firebase/firestore';23async function safeUpdate(4 collectionName: string,5 docId: string,6 data: Record<string, any>7): Promise<{ success: boolean; error?: string }> {8 try {9 await updateDoc(doc(db, collectionName, docId), data);10 return { success: true };11 } catch (err) {12 const error = err as FirestoreError;13 if (error.code === 'not-found') {14 return { success: false, error: 'Document does not exist.' };15 }16 if (error.code === 'permission-denied') {17 return { success: false, error: 'You do not have permission to update this document.' };18 }19 return { success: false, error: error.message };20 }21}Expected result: Errors are caught and returned as structured objects instead of crashing the application.
Complete working example
1import {2 doc,3 updateDoc,4 setDoc,5 increment,6 arrayUnion,7 arrayRemove,8 serverTimestamp,9 FirestoreError,10} from 'firebase/firestore';11import { db } from './firebase';1213// Partial update — fails if document does not exist14export async function updateFields(15 path: string,16 id: string,17 data: Record<string, any>18) {19 const ref = doc(db, path, id);20 await updateDoc(ref, { ...data, updatedAt: serverTimestamp() });21}2223// Upsert — creates or merges24export async function upsertDocument(25 path: string,26 id: string,27 data: Record<string, any>28) {29 const ref = doc(db, path, id);30 await setDoc(ref, { ...data, updatedAt: serverTimestamp() }, { merge: true });31}3233// Atomic counter increment34export async function incrementCounter(35 path: string,36 id: string,37 field: string,38 amount: number = 139) {40 await updateDoc(doc(db, path, id), { [field]: increment(amount) });41}4243// Atomic array operations44export async function addToArray(45 path: string,46 id: string,47 field: string,48 values: any[]49) {50 await updateDoc(doc(db, path, id), { [field]: arrayUnion(...values) });51}5253export async function removeFromArray(54 path: string,55 id: string,56 field: string,57 values: any[]58) {59 await updateDoc(doc(db, path, id), { [field]: arrayRemove(...values) });60}6162// Safe update with error handling63export async function safeUpdate(64 path: string,65 id: string,66 data: Record<string, any>67): Promise<{ success: boolean; error?: string }> {68 try {69 await updateDoc(doc(db, path, id), {70 ...data,71 updatedAt: serverTimestamp(),72 });73 return { success: true };74 } catch (err) {75 const e = err as FirestoreError;76 return { success: false, error: e.message };77 }78}Common mistakes when updating a Document in Firestore
Why it's a problem: Using setDoc() without { merge: true } and accidentally overwriting the entire document
How to avoid: Always pass { merge: true } as the third argument to setDoc() when you only want to update specific fields: setDoc(ref, data, { merge: true }).
Why it's a problem: Passing a nested object to updateDoc() instead of using dot notation, which replaces the entire nested object
How to avoid: Use dot-separated paths for nested updates: { 'address.city': 'NYC' } instead of { address: { city: 'NYC' } }.
Why it's a problem: Calling updateDoc() on a document that does not exist, causing a not-found error
How to avoid: Use setDoc() with merge: true if the document may not exist. Or check existence first with getDoc() before calling updateDoc().
Why it's a problem: Reading a document first to increment a counter instead of using the atomic increment() function
How to avoid: Use increment(1) which runs atomically on the server. Reading, modifying, and writing creates a race condition in concurrent environments.
Best practices
- Use updateDoc() when you know the document exists and want a partial update; use setDoc with merge for upserts
- Always add a serverTimestamp() field like updatedAt so you can track when documents were last modified
- Use dot notation for nested field updates to avoid accidentally overwriting sibling fields
- Prefer atomic modifiers (increment, arrayUnion, arrayRemove) over read-then-write patterns to avoid race conditions
- Write security rules that validate which fields can be changed and who can change them
- Wrap all update calls in try-catch to handle not-found and permission-denied errors gracefully
- Batch multiple updates together with writeBatch() when modifying several documents atomically
Still stuck?
Copy one of these prompts to get a personalized, step-by-step explanation.
Show me how to update Firestore documents using the Firebase v9 modular SDK. Include updateDoc for partial updates, setDoc with merge for upserts, atomic increment, arrayUnion, arrayRemove, serverTimestamp, nested field updates with dot notation, and security rules.
Write TypeScript utility functions for Firestore document updates: partial updates with updateDoc, upserts with setDoc merge, atomic counter increment, array operations with arrayUnion/arrayRemove, and nested field dot notation. Use Firebase modular SDK v9+ imports.
Frequently asked questions
What is the difference between updateDoc and setDoc with merge?
updateDoc() fails with a not-found error if the document does not exist. setDoc() with { merge: true } creates the document if it is missing and merges fields if it exists. Use updateDoc when you expect the document to exist; use setDoc merge for upsert behavior.
Can I update a field inside a map without overwriting other map fields?
Yes. Use dot notation: updateDoc(ref, { 'address.city': 'NYC' }). This updates only the city field inside the address map without affecting other fields like state or zip.
Does updateDoc trigger onSnapshot listeners?
Yes. Any change made with updateDoc or setDoc triggers all active onSnapshot listeners on that document or any query that includes it. Listeners receive the updated data in real time.
Can I update multiple documents at once?
Use writeBatch() to update up to 500 documents atomically in a single operation. For more than 500 documents, split them into multiple batches.
What happens if I update a document while offline?
With offline persistence enabled (default on mobile, opt-in on web), the update is stored locally and synced to the server when the device reconnects. Local listeners see the update immediately.
How do I remove a single field from a document?
Import deleteField() and pass it as the value: updateDoc(ref, { fieldToRemove: deleteField() }). This removes the field entirely from the document.
Can RapidDev help design my Firestore data model and update patterns?
Yes. RapidDev can help you design efficient Firestore schemas, implement atomic update patterns, write security rules, and optimize for cost and performance.
Talk to an Expert
Our team has built 600+ apps. Get personalized help with your project.
Book a free consultation