Skip to content

Service Worker Fetch Event — Intercepting Network Requests

DodaTech Updated 2026-06-28 6 min read

In this tutorial, you will learn about Service Worker Fetch Event. We cover key concepts, practical examples, and best practices to help you master this topic.

The fetch event intercepts every network request from your PWA, allowing you to serve cached responses, fetch from the network, or combine both strategies for optimal performance and offline support.

What You'll Learn

By the end of this tutorial, you will understand how to intercept fetch events, differentiate between request types, implement basic Caching strategies, and handle errors gracefully.

Why It Matters

The fetch event is where your PWA's performance and reliability come to life. Without proper fetch handling, your service worker cannot serve cached content, handle offline scenarios, or optimize network usage. The fetch Strategy you choose directly impacts user experience.

Real-World Use

A weather PWA intercepts fetch requests for the forecast API. It serves cached data instantly while fetching fresh data in the background. Users see yesterday's forecast immediately, which updates to today's within seconds — no loading spinner ever appears.

Fetch Event Basics

Every network request from pages controlled by your service worker triggers a fetch event. You intercept it with event.respondWith() and return a Response object.

Fetch Event Flow
    Page makes network request (CSS, JS, API, image)
         ↓
    Service worker receives 'fetch' event
         ↓
    ╔═══════════════════════════╗
    ║  event.respondWith(      ║
    ║    return Response or     ║
    ║    fallback               ║
    ║  )                        ║
    ╚═══════════════════════════╝
         ↓
    Response returned to page

Think of the fetch event like a mail sorter at a post office. Every letter (request) comes to the sorter first. The sorter decides: deliver from local storage (cache), fetch from the sender (network), or send a standard reply (fallback).

Basic Fetch Handler

self.addEventListener('fetch', event => {
    console.log('Fetch requested:', event.request.url);

    event.respondWith(
        caches.match(event.request)
            .then(cachedResponse => {
                if (cachedResponse) {
                    console.log('Cache hit:', event.request.url);
                    return cachedResponse;
                }

                console.log('Cache miss, fetching:', event.request.url);
                return fetch(event.request)
                    .then(response => {
                        // Cache the response for next time
                        const responseClone = response.clone();
                        caches.open('dynamic-cache').then(cache => {
                            cache.put(event.request, responseClone);
                        });
                        return response;
                    })
                    .catch(error => {
                        console.log('Network failed:', error.message);
                        return caches.match('/offline.html');
                    });
            })
    );
});

Output:

Fetch requested: https://example.com/styles/main.css
Cache hit: https://example.com/styles/main.css
Fetch requested: https://example.com/api/data
Cache miss, fetching: https://example.com/api/data
Network failed: TypeError: Failed to fetch
Served offline fallback

Differentiating Request Types

Different resource types need different strategies. Check the request destination or URL:

self.addEventListener('fetch', event => {
    const { request } = event;
    const url = new URL(request.url);

    // Skip non-GET requests
    if (request.method !== 'GET') return;

    // Same-origin requests
    if (url.origin === location.origin) {
        if (request.destination === 'style') {
            // Cache-first for CSS
            event.respondWith(cacheFirst(request));
        } else if (request.destination === 'script') {
            // Cache-first for JS
            event.respondWith(cacheFirst(request));
        } else if (request.destination === 'image') {
            // Cache-first for images
            event.respondWith(cacheFirst(request));
        } else if (url.pathname.startsWith('/api/')) {
            // Network-first for API calls
            event.respondWith(networkFirst(request));
        } else {
            // Network-first for navigation
            event.respondWith(networkFirst(request));
        }
    } else {
        // Cross-origin: network-only with timeout
        event.respondWith(networkWithTimeout(request, 3000));
    }
});

function cacheFirst(request) {
    return caches.match(request).then(cached => {
        return cached || fetch(request);
    });
}

function networkFirst(request) {
    return fetch(request).catch(() => {
        return caches.match(request);
    });
}

function networkWithTimeout(request, timeout) {
    const timeoutPromise = new Promise((_, reject) => {
        setTimeout(() => reject(new Error('Timeout')), timeout);
    });

    return Promise.race([
        fetch(request),
        timeoutPromise
    ]).catch(() => caches.match(request));
}

Handling Different HTTP Methods

Only GET requests are cacheable. For POST, PUT, DELETE requests, always go to the network:

self.addEventListener('fetch', event => {
    const { request } = event;

    // Only handle GET requests
    if (request.method !== 'GET') {
        return;  // Let the browser handle normally
    }

    event.respondWith(
        caches.match(request)
            .then(cached => cached || fetch(request))
            .catch(() => caches.match('/offline.html'))
    );
});

Responding with Custom Responses

You can create synthetic responses for offline scenarios:

self.addEventListener('fetch', event => {
    const url = new URL(event.request.url);

    // Generate an inline SVG placeholder for missing images
    if (event.request.destination === 'image') {
        event.respondWith(
            caches.match(event.request)
                .then(cached => {
                    if (cached) return cached;

                    // Return a simple placeholder SVG
                    return new Response(
                        `<svg xmlns="http://www.w3.org/2000/svg"
                             width="400" height="300"
                             viewBox="0 0 400 300">
                            <rect fill="#eee" width="400" height="300"/>
                            <text fill="#999" font-size="20"
                                  x="50%" y="50%" text-anchor="middle"
                                  dominant-baseline="middle">
                                Image Offline
                            </text>
                         </svg>`,
                        { headers: { 'Content-Type': 'image/svg+xml' } }
                    );
                })
        );
    }
});

Logging and Analytics

Track which requests hit cache versus network:

self.addEventListener('fetch', event => {
    const startTime = performance.now();

    event.respondWith(
        caches.match(event.request)
            .then(cached => {
                if (cached) {
                    logMetric('cache-hit', event.request.url, performance.now() - startTime);
                    return cached;
                }
                return fetch(event.request).then(response => {
                    logMetric('cache-miss', event.request.url, performance.now() - startTime);
                    return response;
                });
            })
    );
});

function logMetric(type, url, duration) {
    console.log(`[${type}] ${url} (${duration.toFixed(0)}ms)`);

    // You could also send this to an analytics endpoint
    // But be careful not to create infinite loops
    // Use a separate cache or Background Sync for analytics
}

Output:

[cache-hit] https://example.com/styles/main.css (2ms)
[cache-miss] https://example.com/api/data (340ms)
[cache-hit] https://example.com/logo.png (1ms)

Common Mistakes

  1. Not returning early for non-GET requests. POST requests should not be intercepted. Always check request.method !== 'GET' and return early.
  2. Making fetch handlers too slow. Complex sync operations in the fetch handler block the response. Keep fetch handlers lean and use caches.match() as your primary operation.
  3. Caching opaque responses without checking. Cross-origin fetch responses may be opaque (status 0). Do not cache them blindly. Verify the response is valid.
  4. Modifying request headers in the service worker. Some headers cannot be modified. Use caution when creating new Request objects.
  5. Forgetting to clone the response. A response body can only be consumed once. Clone before caching if you also return the original.

Practice Questions

  1. What does event.respondWith() do in the fetch handler?
  2. Why should you skip non-GET requests in the fetch event?
  3. What is the difference between request.destination and URL path matching?
  4. Why must you clone a response before caching it?
  5. How would you implement a timeout for network requests in the fetch handler?

Challenge: Write a fetch handler that applies three different strategies: cache-first for CSS/JS/images, network-first for API calls, and a 5-second timeout for cross-origin resources. Log each decision.

FAQ

Can a service worker intercept requests from other origins?

Yes, fetch events fire for cross-origin requests from your page. However, you can only cache responses that your page has CORS access to.

What happens if I do not call event.respondWith()?

The browser handles the request normally as if there were no service worker. The fetch event listener is optional.

Can I modify request headers before fetching?

You can create a new Request object with modified headers. However, some headers are forbidden and cannot be modified. Always test your modifications.

How do I handle redirects in the fetch handler?

Redirects are handled automatically. You can check response.redirected and response.url to see if a redirect occurred and cache the final URL.

Does the fetch event fire for navigation requests?

Yes, navigation requests (loading a page) trigger a fetch event. You can serve cached HTML pages when offline.

Mini Project

Create a fetch handler that applies three strategies based on request type: cache-first for static assets, network-first for API calls, and a fallback to an offline page for navigation requests. Test by registering the service worker, checking DevTools network tab for cache hits, and verifying offline behavior.

What's Next

You now know how to intercept requests. Learn about the Cache Storage API to understand how the cache works under the hood.

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro