# How to Update a Document in Firestore

- Tool: Firebase
- Difficulty: Intermediate
- Time required: 12-18 min
- Compatibility: Firebase JS SDK v9+, Firestore on all Firebase plans
- Last updated: March 2026

## TL;DR

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.

## Before you start

- 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

### 1. 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.

```
import { doc, updateDoc } from 'firebase/firestore';
import { db } from './firebase';

async function updateUserName(userId: string, newName: string) {
  const userRef = doc(db, 'users', userId);

  await updateDoc(userRef, {
    displayName: newName,
    updatedAt: new Date(),
  });

  console.log('User name updated successfully');
}
```

**Expected result:** The displayName and updatedAt fields are updated, and all other fields on the document remain unchanged.

### 2. 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.

```
import { doc, setDoc } from 'firebase/firestore';

async function upsertUserProfile(userId: string, data: Record<string, any>) {
  const userRef = doc(db, 'users', userId);

  // Creates the document if it doesn't exist,
  // or merges fields if it does
  await setDoc(userRef, {
    ...data,
    lastSeen: new Date(),
  }, { merge: true });
}
```

**Expected result:** The document is created with the provided data if new, or the specified fields are merged into the existing document.

### 3. 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.

```
import { doc, updateDoc, increment, arrayUnion, arrayRemove, serverTimestamp } from 'firebase/firestore';

// Increment a counter without reading current value
await updateDoc(doc(db, 'posts', 'post123'), {
  viewCount: increment(1),
  lastViewed: serverTimestamp(),
});

// Add a tag to an array (only if not already present)
await updateDoc(doc(db, 'posts', 'post123'), {
  tags: arrayUnion('firebase'),
});

// Remove a tag from an array
await updateDoc(doc(db, 'posts', 'post123'), {
  tags: arrayRemove('deprecated'),
});
```

**Expected result:** Fields are modified atomically on the server without needing to read the current document value first.

### 4. 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.

```
import { doc, updateDoc } from 'firebase/firestore';

// Document structure: { address: { city: 'NYC', state: 'NY', zip: '10001' } }

// CORRECT: Update only the city, keep state and zip
await updateDoc(doc(db, 'users', 'user123'), {
  'address.city': 'Los Angeles',
  'address.state': 'CA',
});

// WRONG: This replaces the entire address object, deleting zip
// await updateDoc(doc(db, 'users', 'user123'), {
//   address: { city: 'Los Angeles', state: 'CA' },
// });
```

**Expected result:** Only the specified nested fields are updated while sibling fields in the same nested object remain untouched.

### 5. 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.

```
// firestore.rules
rules_version = '2';
service cloud.firestore {
  match /databases/{database}/documents {
    match /users/{userId} {
      // Only the user can update their own document
      allow update: if request.auth != null
        && request.auth.uid == userId
        // Prevent users from changing their role field
        && request.resource.data.role == resource.data.role;
    }

    match /posts/{postId} {
      // Only the author can update their post
      allow update: if request.auth != null
        && request.auth.uid == resource.data.authorId;
    }
  }
}
```

**Expected result:** Only authorized users can update documents, and protected fields like role cannot be changed by the client.

### 6. 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.

```
import { doc, updateDoc, FirestoreError } from 'firebase/firestore';

async function safeUpdate(
  collectionName: string,
  docId: string,
  data: Record<string, any>
): Promise<{ success: boolean; error?: string }> {
  try {
    await updateDoc(doc(db, collectionName, docId), data);
    return { success: true };
  } catch (err) {
    const error = err as FirestoreError;
    if (error.code === 'not-found') {
      return { success: false, error: 'Document does not exist.' };
    }
    if (error.code === 'permission-denied') {
      return { success: false, error: 'You do not have permission to update this document.' };
    }
    return { success: false, error: error.message };
  }
}
```

**Expected result:** Errors are caught and returned as structured objects instead of crashing the application.

## Complete code example

File: `firestore-update.ts`

```typescript
import {
  doc,
  updateDoc,
  setDoc,
  increment,
  arrayUnion,
  arrayRemove,
  serverTimestamp,
  FirestoreError,
} from 'firebase/firestore';
import { db } from './firebase';

// Partial update — fails if document does not exist
export async function updateFields(
  path: string,
  id: string,
  data: Record<string, any>
) {
  const ref = doc(db, path, id);
  await updateDoc(ref, { ...data, updatedAt: serverTimestamp() });
}

// Upsert — creates or merges
export async function upsertDocument(
  path: string,
  id: string,
  data: Record<string, any>
) {
  const ref = doc(db, path, id);
  await setDoc(ref, { ...data, updatedAt: serverTimestamp() }, { merge: true });
}

// Atomic counter increment
export async function incrementCounter(
  path: string,
  id: string,
  field: string,
  amount: number = 1
) {
  await updateDoc(doc(db, path, id), { [field]: increment(amount) });
}

// Atomic array operations
export async function addToArray(
  path: string,
  id: string,
  field: string,
  values: any[]
) {
  await updateDoc(doc(db, path, id), { [field]: arrayUnion(...values) });
}

export async function removeFromArray(
  path: string,
  id: string,
  field: string,
  values: any[]
) {
  await updateDoc(doc(db, path, id), { [field]: arrayRemove(...values) });
}

// Safe update with error handling
export async function safeUpdate(
  path: string,
  id: string,
  data: Record<string, any>
): Promise<{ success: boolean; error?: string }> {
  try {
    await updateDoc(doc(db, path, id), {
      ...data,
      updatedAt: serverTimestamp(),
    });
    return { success: true };
  } catch (err) {
    const e = err as FirestoreError;
    return { success: false, error: e.message };
  }
}
```

## Common mistakes

- **Using setDoc() without { merge: true } and accidentally overwriting the entire document** — undefined Fix: Always pass { merge: true } as the third argument to setDoc() when you only want to update specific fields: setDoc(ref, data, { merge: true }).
- **Passing a nested object to updateDoc() instead of using dot notation, which replaces the entire nested object** — undefined Fix: Use dot-separated paths for nested updates: { 'address.city': 'NYC' } instead of { address: { city: 'NYC' } }.
- **Calling updateDoc() on a document that does not exist, causing a not-found error** — undefined Fix: Use setDoc() with merge: true if the document may not exist. Or check existence first with getDoc() before calling updateDoc().
- **Reading a document first to increment a counter instead of using the atomic increment() function** — undefined Fix: 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

## 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.

---

Source: https://www.rapidevelopers.com/firebase-tutorial/how-to-update-a-document-in-firestore
© RapidDev — https://www.rapidevelopers.com/firebase-tutorial/how-to-update-a-document-in-firestore
