Skip to content

Bits of .NET

Daily micro-tips for C#, SQL, performance, and scalable backend engineering.

  • Asp.Net Core
  • C#
  • SQL
  • JavaScript
  • CSS
  • About
  • ErcanOPAK.com
  • No Access
  • Privacy Policy
JavaScript

JavaScript Fetch API Secret: How AbortController Stops Memory Leaks in Single Page Apps

- 05.02.26 - ErcanOPAK

Single Page Applications leaking memory from unfinished fetch requests? AbortController is your solution to clean up abandoned API calls.

The Memory Leak Problem:

// Common React/Vue pattern causing leaks
async function fetchUserData(userId) {
    const response = await fetch(`/api/users/${userId}`);
    const data = await response.json();
    setUserData(data); // State update
}

// User clicks between tabs quickly:
// Tab 1: fetchUserData(1) - starts
// Tab 2: fetchUserData(2) - starts before #1 finishes
// Tab 3: fetchUserData(3) - starts before #1 and #2 finish

// Result: 3 parallel requests, but only last one matters
// Previous requests continue, waste bandwidth, cause race conditions
// Memory leak: response handlers stay in memory

Solution: AbortController Pattern

// Create abort controller for each request
let controller = new AbortController();

async function fetchUserDataSafe(userId) {
    // Cancel previous request if still running
    if (controller) {
        controller.abort();
    }
    
    // Create new controller for this request
    controller = new AbortController();
    
    try {
        const response = await fetch(`/api/users/${userId}`, {
            signal: controller.signal // Attach abort signal
        });
        
        if (!response.ok) {
            throw new Error(`HTTP ${response.status}`);
        }
        
        const data = await response.json();
        setUserData(data);
        
    } catch (error) {
        // Check if error is from abort (not network error)
        if (error.name === 'AbortError') {
            console.log('Request was aborted');
            return; // Don't show error for aborted requests
        }
        
        // Handle real errors
        console.error('Fetch error:', error);
        showError(error.message);
    }
}

React Hook Implementation:

import { useEffect, useRef } from 'react';

function useAbortableFetch() {
    const controllers = useRef(new Map());
    
    const fetchWithAbort = async (url, options = {}, key = 'default') => {
        // Cancel previous request with same key
        if (controllers.current.has(key)) {
            controllers.current.get(key).abort();
        }
        
        const controller = new AbortController();
        controllers.current.set(key, controller);
        
        try {
            const response = await fetch(url, {
                ...options,
                signal: controller.signal
            });
            
            if (!response.ok) {
                throw new Error(`HTTP ${response.status}`);
            }
            
            const data = await response.json();
            controllers.current.delete(key); // Clean up
            return { success: true, data };
            
        } catch (error) {
            controllers.current.delete(key); // Clean up
            if (error.name === 'AbortError') {
                return { success: false, aborted: true };
            }
            return { success: false, error };
        }
    };
    
    // Cleanup on unmount
    useEffect(() => {
        return () => {
            controllers.current.forEach(controller => controller.abort());
            controllers.current.clear();
        };
    }, []);
    
    return fetchWithAbort;
}

// Usage in component
function UserProfile({ userId }) {
    const fetchWithAbort = useAbortableFetch();
    const [user, setUser] = useState(null);
    
    useEffect(() => {
        const loadUser = async () => {
            const result = await fetchWithAbort(
                `/api/users/${userId}`,
                {},
                `user-${userId}` // Unique key per user
            );
            
            if (result.success) {
                setUser(result.data);
            } else if (!result.aborted) {
                console.error('Failed:', result.error);
            }
        };
        
        loadUser();
        
        // Cleanup: abort when userId changes or component unmounts
        return () => {
            // Already handled by hook
        };
    }, [userId, fetchWithAbort]);
    
    return 
{/* render */}
; }

Advanced: Timeout with AbortController

// Add timeout to fetch requests
async function fetchWithTimeout(url, options = {}, timeout = 10000) {
    const controller = new AbortController();
    const timeoutId = setTimeout(() => controller.abort(), timeout);
    
    try {
        const response = await fetch(url, {
            ...options,
            signal: controller.signal
        });
        
        clearTimeout(timeoutId);
        
        if (!response.ok) {
            throw new Error(`HTTP ${response.status}`);
        }
        
        return await response.json();
        
    } catch (error) {
        clearTimeout(timeoutId);
        if (error.name === 'AbortError') {
            throw new Error(`Request timeout after ${timeout}ms`);
        }
        throw error;
    }
}

// Usage with retry logic
async function fetchWithRetry(url, retries = 3, timeout = 5000) {
    for (let i = 0; i < retries; i++) {
        try {
            return await fetchWithTimeout(url, {}, timeout);
        } catch (error) {
            if (i === retries - 1) throw error;
            
            // Exponential backoff
            const delay = Math.pow(2, i) * 1000;
            await new Promise(resolve => setTimeout(resolve, delay));
            
            console.log(`Retry ${i + 1}/${retries} for ${url}`);
        }
    }
}

Real-World SPA Implementation:

// Dashboard with multiple real-time updates
class SafeDataFetcher {
    constructor() {
        this.controllers = new Map();
        this.cache = new Map();
        this.cacheTime = new Map();
    }
    
    async fetchData(endpoint, params = {}, cacheDuration = 60000) {
        const cacheKey = `${endpoint}:${JSON.stringify(params)}`;
        
        // Return cached data if valid
        const cachedTime = this.cacheTime.get(cacheKey);
        if (cachedTime && Date.now() - cachedTime < cacheDuration) {
            return this.cache.get(cacheKey);
        }
        
        // Cancel previous request for same endpoint
        if (this.controllers.has(cacheKey)) {
            this.controllers.get(cacheKey).abort();
        }
        
        const controller = new AbortController();
        this.controllers.set(cacheKey, controller);
        
        try {
            const queryString = new URLSearchParams(params).toString();
            const url = queryString ? `${endpoint}?${queryString}` : endpoint;
            
            const response = await fetch(url, {
                signal: controller.signal,
                headers: {
                    'Accept': 'application/json',
                    'Cache-Control': 'no-cache'
                }
            });
            
            if (!response.ok) {
                throw new Error(`HTTP ${response.status}: ${response.statusText}`);
            }
            
            const data = await response.json();
            
            // Cache the result
            this.cache.set(cacheKey, data);
            this.cacheTime.set(cacheKey, Date.now());
            
            // Clean up controller
            this.controllers.delete(cacheKey);
            
            return data;
            
        } catch (error) {
            this.controllers.delete(cacheKey);
            
            if (error.name === 'AbortError') {
                console.log(`Request for ${cacheKey} was aborted`);
                return null;
            }
            
            // Re-throw for caller to handle
            throw error;
        }
    }
    
    abortAll() {
        this.controllers.forEach(controller => controller.abort());
        this.controllers.clear();
    }
    
    clearCache() {
        this.cache.clear();
        this.cacheTime.clear();
    }
}

// Usage in dashboard
const fetcher = new SafeDataFetcher();

// Multiple concurrent safe requests
Promise.all([
    fetcher.fetchData('/api/stats', { period: 'daily' }),
    fetcher.fetchData('/api/users/active'),
    fetcher.fetchData('/api/orders/recent')
])
.then(([stats, users, orders]) => {
    updateDashboard({ stats, users, orders });
})
.catch(error => {
    console.error('Dashboard load failed:', error);
});

// Clean up when leaving dashboard
window.addEventListener('beforeunload', () => {
    fetcher.abortAll();
});

Related posts:

Get and Use TextBox Values with Javascript

How to use column search in datatable when responsive is false

AJAX — 204 Responses Break JSON Parsing

Post Views: 4

Post navigation

SQL Server Indexing Secret: How Covered Indexes Can Make Queries 100x Faster
.NET Core Configuration Magic: How IOptions Pattern Solves Multi-Environment Headaches

Leave a Reply Cancel reply

Your email address will not be published. Required fields are marked *

April 2026
M T W T F S S
 12345
6789101112
13141516171819
20212223242526
27282930  
« Mar    

Most Viewed Posts

  • Get the User Name and Domain Name from an Email Address in SQL (950)
  • How to add default value for Entity Framework migrations for DateTime and Bool (858)
  • Get the First and Last Word from a String or Sentence in SQL (836)
  • How to select distinct rows in a datatable in C# (805)
  • How to make theater mode the default for Youtube (753)
  • Add Constraint to SQL Table to ensure email contains @ (578)
  • How to enable, disable and check if Service Broker is enabled on a database in SQL Server (564)
  • Average of all values in a column that are not zero in SQL (531)
  • How to use Map Mode for Vertical Scroll Mode in Visual Studio (489)
  • Find numbers with more than two decimal places in SQL (447)

Recent Posts

  • C#: Use Init-Only Setters for Immutable Objects After Construction
  • C#: Use Expression-Bodied Members for Concise Single-Line Methods
  • C#: Enable Nullable Reference Types to Eliminate Null Reference Exceptions
  • C#: Use Record Types for Immutable Data Objects
  • SQL: Use CTEs for Readable Complex Queries
  • SQL: Use Window Functions for Advanced Analytical Queries
  • .NET Core: Use Background Services for Long-Running Tasks
  • .NET Core: Use Minimal APIs for Lightweight HTTP Services
  • Git: Use Cherry-Pick to Apply Specific Commits Across Branches
  • Git: Use Interactive Rebase to Clean Up Commit History Before Merge

Most Viewed Posts

  • Get the User Name and Domain Name from an Email Address in SQL (950)
  • How to add default value for Entity Framework migrations for DateTime and Bool (858)
  • Get the First and Last Word from a String or Sentence in SQL (836)
  • How to select distinct rows in a datatable in C# (805)
  • How to make theater mode the default for Youtube (753)

Recent Posts

  • C#: Use Init-Only Setters for Immutable Objects After Construction
  • C#: Use Expression-Bodied Members for Concise Single-Line Methods
  • C#: Enable Nullable Reference Types to Eliminate Null Reference Exceptions
  • C#: Use Record Types for Immutable Data Objects
  • SQL: Use CTEs for Readable Complex Queries

Social

  • ErcanOPAK.com
  • GoodReads
  • LetterBoxD
  • Linkedin
  • The Blog
  • Twitter
© 2026 Bits of .NET | Built with Xblog Plus free WordPress theme by wpthemespace.com