javascript

examples

examples.js
/**
 * AbortController & Cancellation - Examples
 *
 * Comprehensive examples of cancelling async operations in JavaScript
 */

// ============================================
// EXAMPLE 1: Basic AbortController Usage
// ============================================

/**
 * Basic fetch cancellation
 */
function basicAbortExample() {
  const controller = new AbortController();
  const signal = controller.signal;

  // Start the fetch
  fetch('https://jsonplaceholder.typicode.com/posts', { signal })
    .then((response) => response.json())
    .then((data) => console.log('Data received:', data.length, 'posts'))
    .catch((error) => {
      if (error.name === 'AbortError') {
        console.log('Fetch was cancelled');
      } else {
        console.error('Fetch error:', error);
      }
    });

  // Cancel after 100ms
  setTimeout(() => {
    controller.abort();
    console.log('Abort signal sent');
  }, 100);
}

// ============================================
// EXAMPLE 2: Fetch with Timeout
// ============================================

/**
 * Fetch wrapper with automatic timeout
 */
async function fetchWithTimeout(url, options = {}, timeout = 5000) {
  const controller = new AbortController();
  const { signal } = controller;

  // Merge any existing signal with our timeout signal
  if (options.signal) {
    options.signal.addEventListener('abort', () => {
      controller.abort(options.signal.reason);
    });
  }

  const timeoutId = setTimeout(() => {
    controller.abort(`Request timeout after ${timeout}ms`);
  }, timeout);

  try {
    const response = await fetch(url, { ...options, signal });
    clearTimeout(timeoutId);

    if (!response.ok) {
      throw new Error(`HTTP error: ${response.status}`);
    }

    return response;
  } catch (error) {
    clearTimeout(timeoutId);
    throw error;
  }
}

// Usage example
async function fetchWithTimeoutDemo() {
  try {
    const response = await fetchWithTimeout(
      'https://jsonplaceholder.typicode.com/posts',
      {},
      3000
    );
    const data = await response.json();
    console.log('Fetched with timeout:', data.length, 'posts');
  } catch (error) {
    if (error.name === 'AbortError') {
      console.log('Request timed out:', error.message);
    } else {
      console.error('Error:', error);
    }
  }
}

// ============================================
// EXAMPLE 3: Modern Timeout with AbortSignal.timeout()
// ============================================

/**
 * Using the built-in AbortSignal.timeout() (ES2022+)
 */
async function modernTimeoutExample() {
  try {
    // Automatically aborts after 5 seconds
    const response = await fetch('https://jsonplaceholder.typicode.com/posts', {
      signal: AbortSignal.timeout(5000),
    });
    const data = await response.json();
    console.log('Modern timeout fetch:', data.length, 'posts');
  } catch (error) {
    if (error.name === 'TimeoutError') {
      console.log('Request timed out');
    } else if (error.name === 'AbortError') {
      console.log('Request aborted');
    } else {
      console.error('Error:', error);
    }
  }
}

// ============================================
// EXAMPLE 4: Cancellable Fetch Class
// ============================================

class CancellableFetch {
  constructor() {
    this.controller = null;
  }

  async fetch(url, options = {}) {
    // Cancel any existing request
    this.cancel('New request started');

    // Create new controller
    this.controller = new AbortController();

    try {
      const response = await fetch(url, {
        ...options,
        signal: this.controller.signal,
      });

      if (!response.ok) {
        throw new Error(`HTTP ${response.status}`);
      }

      return await response.json();
    } catch (error) {
      if (error.name === 'AbortError') {
        return null; // Cancelled, return null
      }
      throw error;
    }
  }

  cancel(reason = 'Cancelled') {
    if (this.controller) {
      this.controller.abort(reason);
      this.controller = null;
    }
  }
}

// Usage
const fetcher = new CancellableFetch();

async function cancellableFetchDemo() {
  // Start first request
  const promise1 = fetcher.fetch(
    'https://jsonplaceholder.typicode.com/posts/1'
  );

  // Start second request (automatically cancels first)
  const promise2 = fetcher.fetch(
    'https://jsonplaceholder.typicode.com/posts/2'
  );

  const result = await promise2;
  console.log('Got result:', result?.title);
}

// ============================================
// EXAMPLE 5: Search with Auto-Cancel
// ============================================

class SearchManager {
  constructor(searchEndpoint) {
    this.endpoint = searchEndpoint;
    this.controller = null;
    this.debounceTimer = null;
    this.debounceMs = 300;
  }

  async search(query) {
    // Clear debounce timer
    if (this.debounceTimer) {
      clearTimeout(this.debounceTimer);
    }

    // Cancel previous request
    if (this.controller) {
      this.controller.abort('New search');
    }

    return new Promise((resolve, reject) => {
      this.debounceTimer = setTimeout(async () => {
        this.controller = new AbortController();

        try {
          const url = `${this.endpoint}?q=${encodeURIComponent(query)}`;
          const response = await fetch(url, {
            signal: this.controller.signal,
          });

          if (!response.ok) {
            throw new Error(`HTTP ${response.status}`);
          }

          const results = await response.json();
          resolve(results);
        } catch (error) {
          if (error.name !== 'AbortError') {
            reject(error);
          }
          // Silently ignore abort errors
          resolve(null);
        }
      }, this.debounceMs);
    });
  }

  cancel() {
    if (this.debounceTimer) {
      clearTimeout(this.debounceTimer);
    }
    if (this.controller) {
      this.controller.abort('Search cancelled');
    }
  }
}

// Simulated usage
const searchManager = new SearchManager('/api/search');

// ============================================
// EXAMPLE 6: Combining Multiple Signals
// ============================================

/**
 * Combine multiple abort signals into one
 * Polyfill for AbortSignal.any()
 */
function combineAbortSignals(...signals) {
  const controller = new AbortController();

  const onAbort = function () {
    controller.abort(this.reason);
    cleanup();
  };

  const cleanup = () => {
    signals.forEach((signal) => {
      signal.removeEventListener('abort', onAbort);
    });
  };

  for (const signal of signals) {
    if (signal.aborted) {
      controller.abort(signal.reason);
      return controller.signal;
    }
    signal.addEventListener('abort', onAbort);
  }

  return controller.signal;
}

// Usage: Cancel on either user action OR timeout
async function fetchWithCombinedSignals(url) {
  const userController = new AbortController();

  // Create timeout signal
  const timeoutSignal = AbortSignal.timeout(10000);

  // Combine signals
  const combinedSignal = combineAbortSignals(
    userController.signal,
    timeoutSignal
  );

  // Expose cancel function
  const cancel = () => userController.abort('User cancelled');

  try {
    const response = await fetch(url, { signal: combinedSignal });
    return await response.json();
  } catch (error) {
    if (error.name === 'AbortError') {
      console.log('Request aborted:', error.message);
    }
    throw error;
  }
}

// ============================================
// EXAMPLE 7: Cancellable Promise Wrapper
// ============================================

class CancellablePromise {
  constructor(executor) {
    this.controller = new AbortController();
    this.settled = false;

    this.promise = new Promise((resolve, reject) => {
      // Handle abort
      this.controller.signal.addEventListener('abort', () => {
        if (!this.settled) {
          reject(
            new DOMException(
              this.controller.signal.reason || 'Cancelled',
              'AbortError'
            )
          );
        }
      });

      // Execute with signal
      const wrappedResolve = (value) => {
        this.settled = true;
        resolve(value);
      };

      const wrappedReject = (error) => {
        this.settled = true;
        reject(error);
      };

      try {
        executor(wrappedResolve, wrappedReject, this.controller.signal);
      } catch (error) {
        wrappedReject(error);
      }
    });
  }

  cancel(reason = 'Cancelled') {
    if (!this.settled) {
      this.controller.abort(reason);
    }
  }

  then(onFulfilled, onRejected) {
    return this.promise.then(onFulfilled, onRejected);
  }

  catch(onRejected) {
    return this.promise.catch(onRejected);
  }

  finally(onFinally) {
    return this.promise.finally(onFinally);
  }
}

// Usage
function cancellablePromiseDemo() {
  const operation = new CancellablePromise(async (resolve, reject, signal) => {
    // Simulate long operation
    for (let i = 0; i < 10; i++) {
      if (signal.aborted) {
        reject(new DOMException('Cancelled', 'AbortError'));
        return;
      }
      await new Promise((r) => setTimeout(r, 500));
    }
    resolve('Operation complete');
  });

  operation
    .then((result) => console.log(result))
    .catch((error) => {
      if (error.name === 'AbortError') {
        console.log('Operation was cancelled');
      }
    });

  // Cancel after 2 seconds
  setTimeout(() => operation.cancel('Took too long'), 2000);
}

// ============================================
// EXAMPLE 8: Event Listener Cleanup with Signal
// ============================================

class ComponentWithEvents {
  constructor(element) {
    this.element = element;
    this.abortController = new AbortController();
  }

  mount() {
    const { signal } = this.abortController;

    // All these listeners will be removed together
    this.element.addEventListener('click', this.handleClick.bind(this), {
      signal,
    });
    this.element.addEventListener(
      'mouseenter',
      this.handleMouseEnter.bind(this),
      { signal }
    );
    this.element.addEventListener(
      'mouseleave',
      this.handleMouseLeave.bind(this),
      { signal }
    );

    window.addEventListener('resize', this.handleResize.bind(this), { signal });
    document.addEventListener('keydown', this.handleKeyDown.bind(this), {
      signal,
    });

    console.log('Component mounted with event listeners');
  }

  unmount() {
    // Single call removes ALL listeners
    this.abortController.abort();
    console.log('Component unmounted, all listeners removed');
  }

  handleClick(e) {
    console.log('Click:', e.target);
  }
  handleMouseEnter() {
    console.log('Mouse enter');
  }
  handleMouseLeave() {
    console.log('Mouse leave');
  }
  handleResize() {
    console.log('Resize');
  }
  handleKeyDown(e) {
    console.log('Key:', e.key);
  }
}

// ============================================
// EXAMPLE 9: Parallel Fetch with Single Cancel
// ============================================

class ParallelFetcher {
  constructor() {
    this.controller = null;
  }

  async fetchAll(urls) {
    // Cancel any previous batch
    this.cancel();

    this.controller = new AbortController();
    const { signal } = this.controller;

    const promises = urls.map(async (url) => {
      const response = await fetch(url, { signal });
      if (!response.ok) {
        throw new Error(`HTTP ${response.status} for ${url}`);
      }
      return response.json();
    });

    return Promise.all(promises);
  }

  async fetchRace(urls) {
    this.cancel();

    this.controller = new AbortController();
    const { signal } = this.controller;

    const promises = urls.map(async (url) => {
      const response = await fetch(url, { signal });
      if (!response.ok) {
        throw new Error(`HTTP ${response.status}`);
      }
      const data = await response.json();
      // Cancel other requests once we have a winner
      this.controller.abort('Race won');
      return data;
    });

    return Promise.race(promises);
  }

  cancel(reason = 'Cancelled') {
    if (this.controller) {
      this.controller.abort(reason);
      this.controller = null;
    }
  }
}

// ============================================
// EXAMPLE 10: Retry with Cancellation
// ============================================

async function fetchWithRetry(url, options = {}) {
  const {
    maxRetries = 3,
    retryDelay = 1000,
    timeout = 5000,
    signal,
    ...fetchOptions
  } = options;

  const controller = new AbortController();

  // Link external signal if provided
  if (signal) {
    signal.addEventListener('abort', () => {
      controller.abort(signal.reason);
    });

    if (signal.aborted) {
      throw new DOMException(signal.reason || 'Aborted', 'AbortError');
    }
  }

  let lastError;

  for (let attempt = 0; attempt <= maxRetries; attempt++) {
    try {
      // Create timeout for this attempt
      const timeoutId = setTimeout(() => {
        controller.abort('Request timeout');
      }, timeout);

      const response = await fetch(url, {
        ...fetchOptions,
        signal: controller.signal,
      });

      clearTimeout(timeoutId);

      if (!response.ok) {
        throw new Error(`HTTP ${response.status}`);
      }

      return await response.json();
    } catch (error) {
      lastError = error;

      // Don't retry on abort
      if (error.name === 'AbortError') {
        throw error;
      }

      // Don't retry on last attempt
      if (attempt === maxRetries) {
        break;
      }

      console.log(
        `Attempt ${attempt + 1} failed, retrying in ${retryDelay}ms...`
      );

      // Wait before retry
      await new Promise((resolve) => setTimeout(resolve, retryDelay));
    }
  }

  throw lastError;
}

// ============================================
// EXAMPLE 11: Cancellable Async Iterator
// ============================================

async function* cancellablePagination(baseUrl, signal) {
  let page = 1;
  let hasMore = true;

  while (hasMore) {
    // Check if cancelled before each request
    if (signal?.aborted) {
      throw new DOMException(
        signal.reason || 'Iteration cancelled',
        'AbortError'
      );
    }

    const response = await fetch(`${baseUrl}?page=${page}`, { signal });

    if (!response.ok) {
      throw new Error(`HTTP ${response.status}`);
    }

    const data = await response.json();

    if (data.items.length === 0) {
      hasMore = false;
    } else {
      yield data.items;
      page++;
    }
  }
}

// Usage
async function paginationDemo() {
  const controller = new AbortController();

  try {
    const paginator = cancellablePagination(
      'https://api.example.com/items',
      controller.signal
    );

    for await (const items of paginator) {
      console.log('Got page with', items.length, 'items');

      // Stop after 3 pages
      if (items.pageNumber >= 3) {
        controller.abort('Enough pages');
      }
    }
  } catch (error) {
    if (error.name === 'AbortError') {
      console.log('Pagination stopped:', error.message);
    }
  }
}

// ============================================
// EXAMPLE 12: Upload with Progress and Cancel
// ============================================

class CancellableUpload {
  constructor() {
    this.controller = null;
    this.onProgress = null;
  }

  async upload(url, file, onProgress) {
    this.cancel();

    this.controller = new AbortController();
    this.onProgress = onProgress;

    return new Promise((resolve, reject) => {
      const xhr = new XMLHttpRequest();

      // Handle abort
      this.controller.signal.addEventListener('abort', () => {
        xhr.abort();
        reject(
          new DOMException(
            this.controller.signal.reason || 'Upload cancelled',
            'AbortError'
          )
        );
      });

      xhr.upload.addEventListener('progress', (e) => {
        if (e.lengthComputable && this.onProgress) {
          const percent = (e.loaded / e.total) * 100;
          this.onProgress(percent, e.loaded, e.total);
        }
      });

      xhr.addEventListener('load', () => {
        if (xhr.status >= 200 && xhr.status < 300) {
          resolve(JSON.parse(xhr.responseText));
        } else {
          reject(new Error(`Upload failed: ${xhr.status}`));
        }
      });

      xhr.addEventListener('error', () => {
        reject(new Error('Upload failed'));
      });

      xhr.open('POST', url);

      const formData = new FormData();
      formData.append('file', file);

      xhr.send(formData);
    });
  }

  cancel(reason = 'Upload cancelled') {
    if (this.controller) {
      this.controller.abort(reason);
      this.controller = null;
    }
  }
}

// ============================================
// EXAMPLE 13: WebSocket with Abort
// ============================================

class CancellableWebSocket {
  constructor(url) {
    this.url = url;
    this.ws = null;
    this.controller = new AbortController();
  }

  connect() {
    return new Promise((resolve, reject) => {
      this.controller.signal.addEventListener('abort', () => {
        if (this.ws) {
          this.ws.close();
        }
        reject(new DOMException('Connection cancelled', 'AbortError'));
      });

      if (this.controller.signal.aborted) {
        reject(new DOMException('Already aborted', 'AbortError'));
        return;
      }

      this.ws = new WebSocket(this.url);

      this.ws.onopen = () => resolve(this.ws);
      this.ws.onerror = (error) => reject(error);
    });
  }

  send(data) {
    if (this.controller.signal.aborted) {
      throw new DOMException('Connection closed', 'AbortError');
    }
    this.ws?.send(JSON.stringify(data));
  }

  close(reason = 'Closing connection') {
    this.controller.abort(reason);
    this.ws?.close();
  }
}

// ============================================
// DEMONSTRATION
// ============================================

function demonstrateAbortController() {
  console.log('=== AbortController & Cancellation Examples ===\n');

  console.log('Available examples:');
  const examples = [
    'basicAbortExample() - Basic fetch cancellation',
    'fetchWithTimeoutDemo() - Timeout wrapper',
    'modernTimeoutExample() - AbortSignal.timeout()',
    'cancellableFetchDemo() - Auto-cancelling fetcher',
    'cancellablePromiseDemo() - Cancellable Promise class',
    'CancellableFetch - Class for managing requests',
    'SearchManager - Debounced search with cancel',
    'ComponentWithEvents - Event listener cleanup',
    'ParallelFetcher - Cancel multiple requests',
    'fetchWithRetry() - Retry with cancellation',
    'CancellableUpload - File upload with progress',
    'CancellableWebSocket - WebSocket with abort',
  ];

  examples.forEach((ex, i) => console.log(`${i + 1}. ${ex}`));

  console.log('\nKey APIs:');
  console.log('- new AbortController()');
  console.log('- controller.signal');
  console.log('- controller.abort(reason)');
  console.log('- signal.aborted');
  console.log('- signal.reason');
  console.log('- AbortSignal.timeout(ms)');
  console.log('- AbortSignal.any([signals])');
}

// Export for module usage
if (typeof module !== 'undefined' && module.exports) {
  module.exports = {
    fetchWithTimeout,
    CancellableFetch,
    SearchManager,
    combineAbortSignals,
    CancellablePromise,
    ComponentWithEvents,
    ParallelFetcher,
    fetchWithRetry,
    cancellablePagination,
    CancellableUpload,
    CancellableWebSocket,
    demonstrateAbortController,
  };
}
Examples - JavaScript Tutorial | DeepML