The Secret Life of JavaScript: The Background Sync

 

The Secret Life of JavaScript: The Background Sync

Guaranteed delivery for offline mutations

#JavaScript #Frontend #ServiceWorkers #BackgroundSync




Margaret is a senior software engineer. Timothy is her junior colleague. They work in a grand Victorian library in London — the kind of place where code quality is the unspoken objective, and craftsmanship is the only thing that matters.

Episode 29

The Evaporating Form

Timothy watched his screen with a mixture of pride and dread. The offline architecture was working perfectly for reading data. But then, he tested the "Update Profile" form.

He toggled his network connection to "Offline," filled out three paragraphs of text in the bio section, and clicked "Save."

The DevTools console immediately flashed angry red text: TypeError: Failed to fetch. The application crashed, and the three paragraphs of text evaporated into the digital void.

"Our offline users can read the dashboard perfectly," Timothy said, shaking his head as Margaret approached with her dark roast coffee. "But the second they try to update their profile while on a subway without Wi-Fi, the network request fails and all their form data is instantly destroyed."

The Limit of the Cache API

Margaret looked at the console error.

"You are discovering the hard limit of the Cache API," Margaret explained. "Last week, we built an elegant Stale-While-Revalidate pattern. But the Cache API is strictly designed for GET requests. You cannot cache a POST request. You cannot cache a data mutation. You have built a brilliant reading experience, but an incredibly fragile writing experience."

"So what happens when the user clicks save offline?" Timothy asked. "I can't just pause a standard fetch() until the network comes back."

"No, you can't," Margaret agreed. She picked up a dry-erase marker. "But you can decouple the UI from the network using IndexedDB and the Background Sync API."

The Offline Queue

Margaret drew two boxes on the whiteboard: the Main Thread and the Service Worker.

"Right now, your form submission is tightly coupled to the network," she explained. "When the user clicks save, we need to intercept that payload and immediately write it to IndexedDB—the browser's built-in, persistent local database. That creates a secure offline queue."

"Once the payload is safely in the database," Margaret continued, "we register a sync event with the Service Worker. Think of the Background Sync API as an intelligent, patient event listener. If the network is offline, the API simply queues the event. The moment the browser detects a restored connection—even if the user has already navigated away or closed the tab—it fires the sync event. The Service Worker wakes up, reads the payload from IndexedDB, and executes the fetch in the background."

Guaranteed Delivery

Timothy split the logic. First, in his main application file, he intercepted the form submission, saved the data to IndexedDB, and registered the sync tag.

// main.js - Writing to IndexedDB and Requesting a Sync
form.addEventListener('submit', async (event) => {
  event.preventDefault();
  
  // 1. Save payload to IndexedDB (using the lightweight 'idb' wrapper library)
  await idbDatabase.put('offline-mutations', formData);
  
  // 2. Register the sync event with the Service Worker
  const swRegistration = await navigator.serviceWorker.ready;
  await swRegistration.sync.register('sync-profile-update');
  
  // 3. Optimistic UI: Assume success to keep the app feeling instant
  showToast("Profile saved! We'll sync it when you're back online.");
});

Then, he opened his sw.js file and set up the Service Worker to listen for the network to return.

// sw.js - The Background Sync Handler
self.addEventListener('sync', (event) => {
  // Check if this is the specific sync tag we registered
  if (event.tag === 'sync-profile-update') {
    
    // event.waitUntil() keeps the Service Worker alive until the promise resolves
    event.waitUntil(
      // 1. Retrieve the queued payload from IndexedDB
      idbDatabase.getAll('offline-mutations').then(async (mutations) => {
        
        for (const payload of mutations) {
          // 2. Execute the POST request to the server
          await fetch('/api/profile', { 
            method: 'POST', 
            body: JSON.stringify(payload) 
          });
          
          // 3. Clean the payload out of IndexedDB upon successful delivery
          await idbDatabase.delete('offline-mutations', payload.id);
        }
      })
    );
  }
});

The Patient Worker

"Look at how robust that architecture is," Margaret said. "By passing the data to IndexedDB and utilizing event.waitUntil(), you've guaranteed delivery. The Background Sync API handles the complex logic of monitoring the network state. If the fetch fails, the promise rejects, and the API will automatically retry the sync event later. Just keep in mind that because this API is so powerful, browsers require a secure HTTPS connection to use it."

Timothy tested it. He toggled the DevTools back to "Offline" and filled out the form. He clicked "Save."

The UI instantly showed the success toast. There were no console errors. He closed the browser tab completely. Then, he turned his Wi-Fi back on. In the background, invisible to the user, the browser detected the network and woke the Service Worker up. It pulled the queued data from IndexedDB and successfully posted it to the server.

The application was no longer just offline-readable. It was completely offline-writable.


Aaron Rose is a software engineer and technology writer at tech-reader.blog

Catch up on the latest explainer videos, podcasts, and industry discussions below.


Comments

Popular posts from this blog

Insight: The Great Minimal OS Showdown—DietPi vs Raspberry Pi OS Lite

The New ChatGPT Reason Feature: What It Is and Why You Should Use It

Running AI Models on Raspberry Pi 5 (8GB RAM): What Works and What Doesn't