We use cookies to enhance your experience on the site
CodeWorlds

Fetch API

In the modern Jurassic Park, in addition to local monitoring and control systems, we also need to communicate with external systems and APIs. Whether checking weather forecasts for the park, synchronizing data with global dinosaur databases, or integrating with visitor booking systems — all of this requires a solid method for making HTTP requests. In this exercise we will learn about the Fetch API, the modern interface for making network requests in JavaScript.

What Is the Fetch API?

The Fetch API is a modern, built-in JavaScript interface for making HTTP requests. It replaces older approaches such as XMLHttpRequest, offering a more ergonomic and powerful Promise-based toolset.

Main advantages of the Fetch API:

  • Uses Promises, making it easier to handle asynchronous operations
  • Has a simpler and more intuitive syntax than XMLHttpRequest
  • Offers better control over HTTP requests
  • Allows easy chaining of asynchronous operations
  • Is a browser standard and available in Node.js (from version 18)

Basic Fetch Usage

The basic Fetch syntax is very simple:

1fetch(url)
2  .then(response => response.json())
3  .then(data => console.log(data))
4  .catch(error => console.error('Error:', error));

Let's see how we could use Fetch in our Jurassic Park:

1// Fetching dinosaur data from the API
2function fetchDinosaurData(id) {
3  return fetch(`https://api.jurassic-park.com/dinosaurs/${id}`)
4    .then(response => {
5      // Check that the response is successful
6      if (!response.ok) {
7        throw new Error(`HTTP Error: ${response.status}`);
8      }
9      return response.json();
10    })
11    .then(data => {
12      console.log(`Fetched dinosaur data: ${data.name}`);
13      return data;
14    })
15    .catch(error => {
16      console.error(`Failed to fetch dinosaur #${id} data:`, error);
17      throw error;
18    });
19}
20
21// Using the function
22fetchDinosaurData(42)
23  .then(dino => {
24    console.log(`Dinosaur #42: ${dino.name}, Species: ${dino.species}`);
25    updateInfoPanel(dino);
26  })
27  .catch(error => {
28    displayErrorMessage(`Failed to fetch dinosaur info: ${error.message}`);
29  });

Making Different Types of HTTP Requests

Fetch performs a GET request by default, but we can easily specify a different request type and send data:

1// Updating dinosaur data with a PUT request
2async function updateDinosaurData(id, newData) {
3  try {
4    const response = await fetch(`https://api.jurassic-park.com/dinosaurs/${id}`, {
5      method: 'PUT',
6      headers: {
7        'Content-Type': 'application/json',
8        'Authorization': `Bearer ${apiToken}`
9      },
10      body: JSON.stringify(newData)
11    });
12
13    if (!response.ok) {
14      throw new Error(`Update error: ${response.status}`);
15    }
16
17    const updatedDinosaur = await response.json();
18    console.log(`Updated dinosaur #${id} data`);
19    return updatedDinosaur;
20  } catch (error) {
21    console.error(`Error updating dinosaur #${id}:`, error);
22    throw error;
23  }
24}
25
26// Creating a new dinosaur record with a POST request
27async function addNewDinosaur(data) {
28  try {
29    const response = await fetch('https://api.jurassic-park.com/dinosaurs', {
30      method: 'POST',
31      headers: {
32        'Content-Type': 'application/json',
33        'Authorization': `Bearer ${apiToken}`
34      },
35      body: JSON.stringify(data)
36    });
37
38    if (!response.ok) {
39      throw new Error(`Error adding dinosaur: ${response.status}`);
40    }
41
42    const newDinosaur = await response.json();
43    console.log(`Added new dinosaur #${newDinosaur.id}`);
44    return newDinosaur;
45  } catch (error) {
46    console.error('Error adding dinosaur:', error);
47    throw error;
48  }
49}
50
51// Deleting a dinosaur with a DELETE request
52async function deleteDinosaur(id) {
53  try {
54    const response = await fetch(`https://api.jurassic-park.com/dinosaurs/${id}`, {
55      method: 'DELETE',
56      headers: {
57        'Authorization': `Bearer ${apiToken}`
58      }
59    });
60
61    if (!response.ok) {
62      throw new Error(`Error deleting dinosaur: ${response.status}`);
63    }
64
65    // For DELETE the server may not return any data
66    if (response.status !== 204) { // No Content
67      const data = await response.json();
68      console.log(`Deleted dinosaur #${id}:`, data);
69      return data;
70    }
71
72    console.log(`Deleted dinosaur #${id}`);
73    return { success: true, id };
74  } catch (error) {
75    console.error(`Error deleting dinosaur #${id}:`, error);
76    throw error;
77  }
78}

The Response Object

When we call

fetch()
, it returns a Promise that resolves to a
Response
object. This object contains all information about the HTTP response:

1fetch('https://api.jurassic-park.com/status')
2  .then(response => {
3    console.log('Status:', response.status); // e.g. 200
4    console.log('Status OK:', response.ok); // true for status 200-299
5    console.log('Content type:', response.headers.get('content-type'));
6    console.log('All headers:', [...response.headers.entries()]);
7
8    return response.json(); // Parse response as JSON
9  })
10  .then(data => {
11    console.log('Data:', data);
12  });

The

Response
object offers various methods for processing response data:

| Method | Description | |--------|-------------| | response.json() | Parses content as JSON and returns a Promise with the result | | response.text() | Returns a Promise with content as text | | response.blob() | Returns a Promise with content as a Blob object (binary data) | | response.formData() | Parses content as FormData | | response.arrayBuffer() | Returns a Promise with content as ArrayBuffer |

It is important to remember that you can only call one of these methods on a

Response
object. The response body can only be processed once — trying to call
response.json()
multiple times will throw an error.

Error Handling in Fetch

An important characteristic of the Fetch API is that the Promise returned by

fetch()
will not be rejected for HTTP errors such as 404 or 500. It will only be rejected for network problems such as no connection.

To properly handle HTTP errors, you need to check the

response.ok
property or the
response.status
code:

1fetch('https://api.jurassic-park.com/dinosaurs/999')
2  .then(response => {
3    if (!response.ok) {
4      throw new Error(`HTTP Error: ${response.status}`);
5    }
6    return response.json();
7  })
8  .then(data => {
9    console.log('Data:', data);
10  })
11  .catch(error => {
12    console.error('Error:', error.message);
13    // Here we can handle both HTTP errors and network errors
14  });

You can expand this logic to better handle different status codes:

1fetch('https://api.jurassic-park.com/dinosaurs/999')
2  .then(response => {
3    if (response.status === 404) {
4      throw new Error('Dinosaur not found');
5    } else if (response.status === 401) {
6      throw new Error('Unauthorized — please log in again');
7    } else if (response.status === 403) {
8      throw new Error('No permission for this operation');
9    } else if (!response.ok) {
10      throw new Error(`HTTP Error: ${response.status}`);
11    }
12    return response.json();
13  })
14  .then(/* ... */)
15  .catch(/* ... */);

Fetch Configuration Options

Fetch accepts an optional second argument that allows customizing the request:

1fetch(url, {
2  method: 'POST',           // HTTP method (GET, POST, PUT, DELETE, etc.)
3  headers: {                // HTTP headers
4    'Content-Type': 'application/json',
5    'Authorization': 'Bearer token123'
6  },
7  body: JSON.stringify(data), // Request body (for POST, PUT, etc.)
8  mode: 'cors',             // CORS mode
9  credentials: 'include',   // Whether to include cookies
10  cache: 'no-cache',        // Cache handling strategy
11  redirect: 'follow',       // How to handle redirects
12  referrer: 'no-referrer',  // Referrer header
13  signal: abortController.signal // For cancelling the request
14})

Example with Most Options

1async function makeComplexRequest() {
2  const abortController = new AbortController();
3  const signal = abortController.signal;
4
5  // Set a timeout for the request
6  const timeout = setTimeout(() => {
7    abortController.abort();
8  }, 5000); // 5 seconds
9
10  try {
11    const response = await fetch('https://api.jurassic-park.com/data', {
12      method: 'POST',
13      headers: {
14        'Content-Type': 'application/json',
15        'Authorization': `Bearer ${apiToken}`,
16        'X-Jurassic-Version': '1.2'
17      },
18      body: JSON.stringify({
19        query: 'velociraptor',
20        limit: 10,
21        withDetails: true
22      }),
23      mode: 'cors',
24      credentials: 'include', // Include cookies
25      cache: 'no-cache',      // Do not use cache
26      redirect: 'follow',     // Automatically follow redirects
27      referrer: 'no-referrer',
28      signal                  // For cancelling the request
29    });
30
31    clearTimeout(timeout);
32
33    if (!response.ok) {
34      throw new Error(`HTTP Error: ${response.status}`);
35    }
36
37    return await response.json();
38  } catch (error) {
39    clearTimeout(timeout);
40
41    if (error.name === 'AbortError') {
42      console.error('Request aborted after timeout');
43      throw new Error('Request timed out');
44    }
45
46    console.error('Request error:', error);
47    throw error;
48  }
49}

Cancelling Requests with AbortController

AbortController is an interface that allows cancelling Fetch requests. It is especially useful for implementing timeouts or when the user wants to cancel a long-running request:

1function fetchLargeDinosaurData(query) {
2  // Create a cancellation controller
3  const controller = new AbortController();
4  const { signal } = controller;
5
6  // Set timeout
7  const timeoutId = setTimeout(() => {
8    controller.abort();
9  }, 10000); // 10 seconds
10
11  // Cancel button for the user
12  const cancelButton = document.getElementById('cancel-button');
13  cancelButton.addEventListener('click', () => {
14    controller.abort();
15    clearTimeout(timeoutId);
16    cancelButton.disabled = true;
17    cancelButton.textContent = 'Cancelled';
18  });
19
20  // Make the request with the abort signal
21  return fetch(`https://api.jurassic-park.com/search?q=${query}`, { signal })
22    .then(response => {
23      clearTimeout(timeoutId);
24      if (!response.ok) {
25        throw new Error(`HTTP Error: ${response.status}`);
26      }
27      return response.json();
28    })
29    .then(data => {
30      cancelButton.disabled = true;
31      cancelButton.textContent = 'Done';
32      return data;
33    })
34    .catch(error => {
35      clearTimeout(timeoutId);
36
37      if (error.name === 'AbortError') {
38        console.log('Request was cancelled');
39        return { cancelled: true };
40      }
41
42      throw error;
43    });
44}

Parallel Requests with Promise.all

Sometimes we need to make multiple requests at the same time:

1async function fetchPaddockData(paddockId) {
2  try {
3    const dinosaursPromise = fetch(`https://api.jurassic-park.com/paddocks/${paddockId}/dinosaurs`).then(r => r.json());
4    const statusPromise = fetch(`https://api.jurassic-park.com/paddocks/${paddockId}/status`).then(r => r.json());
5    const historyPromise = fetch(`https://api.jurassic-park.com/paddocks/${paddockId}/history`).then(r => r.json());
6
7    const [dinosaurs, status, history] = await Promise.all([
8      dinosaursPromise,
9      statusPromise,
10      historyPromise
11    ]);
12
13    return { paddockId, dinosaurs, status, history, updatedAt: new Date() };
14  } catch (error) {
15    console.error(`Error fetching data for paddock #${paddockId}:`, error);
16    throw error;
17  }
18}

Handling Different Response Formats

Fetch can handle various data formats, not just JSON:

1// Plain text
2fetch('https://api.jurassic-park.com/logs')
3  .then(response => response.text())
4  .then(text => {
5    console.log('Logs:', text);
6    document.getElementById('logs').textContent = text;
7  });
8
9// Binary data (images, files)
10fetch('https://api.jurassic-park.com/dinosaurs/42/image')
11  .then(response => response.blob())
12  .then(blob => {
13    const imageUrl = URL.createObjectURL(blob);
14    document.getElementById('dino-image').src = imageUrl;
15  });
16
17// Downloading and saving a file (in browser environment)
18fetch('https://api.jurassic-park.com/reports/monthly.pdf')
19  .then(response => response.blob())
20  .then(blob => {
21    const url = URL.createObjectURL(blob);
22    const a = document.createElement('a');
23    a.href = url;
24    a.download = 'monthly-report.pdf';
25    document.body.appendChild(a);
26    a.click();
27    document.body.removeChild(a);
28    URL.revokeObjectURL(url);
29  });

Sending Forms

Fetch can also handle form submissions:

1// Sending form data as JSON
2document.getElementById('dino-form').addEventListener('submit', async function(e) {
3  e.preventDefault();
4
5  const form = e.target;
6  const formData = new FormData(form);
7
8  // Convert FormData to a JavaScript object
9  const formObject = {};
10  for (const [key, value] of formData.entries()) {
11    formObject[key] = value;
12  }
13
14  try {
15    const response = await fetch('https://api.jurassic-park.com/dinosaurs', {
16      method: 'POST',
17      headers: {
18        'Content-Type': 'application/json'
19      },
20      body: JSON.stringify(formObject)
21    });
22
23    if (!response.ok) {
24      throw new Error(`HTTP Error: ${response.status}`);
25    }
26
27    const result = await response.json();
28    alert(`Dinosaur added successfully! ID: ${result.id}`);
29    form.reset();
30  } catch (error) {
31    console.error('Error submitting form:', error);
32    alert(`Error: ${error.message}`);
33  }
34});
35
36// Sending a form with file uploads
37document.getElementById('dino-upload-form').addEventListener('submit', async function(e) {
38  e.preventDefault();
39
40  const form = e.target;
41  const formData = new FormData(form);
42
43  try {
44    const response = await fetch('https://api.jurassic-park.com/dinosaurs/import', {
45      method: 'POST',
46      body: formData // FormData is automatically set as 'multipart/form-data'
47    });
48
49    if (!response.ok) {
50      throw new Error(`HTTP Error: ${response.status}`);
51    }
52
53    const result = await response.json();
54    alert(`Import complete! Added ${result.count} dinosaurs.`);
55  } catch (error) {
56    console.error('Error during import:', error);
57    alert(`Error: ${error.message}`);
58  }
59});

CORS (Cross-Origin Resource Sharing)

CORS is a security mechanism that controls whether a page can make requests to resources on another domain:

1// Basic CORS request
2fetch('https://api.different-domain.com/data', {
3  mode: 'cors' // Default value
4});
5
6// Request with authentication
7fetch('https://api.different-domain.com/protected-data', {
8  mode: 'cors',
9  credentials: 'include' // Send cross-origin cookies
10});

Credential options:

  • omit
    : Do not send cookies (default)
  • same-origin
    : Send cookies only to the same domain
  • include
    : Send cookies to other domains too

Cache Control

Fetch lets you control how the browser uses its cache:

1// Ignore cache, always request from server
2fetch(url, { cache: 'no-store' });
3
4// Check cache first, then request if not found
5fetch(url, { cache: 'default' });
6
7// Check cache first but verify with server that the response is fresh
8fetch(url, { cache: 'no-cache' });
9
10// Use only cache, do not make a network request
11fetch(url, { cache: 'only-if-cached', mode: 'same-origin' });

Practical Patterns with Fetch API

Universal API Request Function

1async function apiRequest(endpoint, options = {}) {
2  const apiBaseUrl = 'https://api.jurassic-park.com';
3  const url = `${apiBaseUrl}${endpoint}`;
4
5  const defaultOptions = {
6    headers: {
7      'Content-Type': 'application/json',
8      'Authorization': `Bearer ${localStorage.getItem('apiToken')}`
9    },
10    mode: 'cors'
11  };
12
13  const mergedOptions = {
14    ...defaultOptions,
15    ...options,
16    headers: {
17      ...defaultOptions.headers,
18      ...(options.headers || {})
19    }
20  };
21
22  if (mergedOptions.body && typeof mergedOptions.body === 'object' && !(mergedOptions.body instanceof FormData)) {
23    mergedOptions.body = JSON.stringify(mergedOptions.body);
24  }
25
26  try {
27    const response = await fetch(url, mergedOptions);
28
29    if (response.status === 401) {
30      await refreshAuthToken();
31      return apiRequest(endpoint, options);
32    }
33
34    if (response.status === 204) {
35      return { success: true };
36    }
37
38    if (!response.ok) {
39      try {
40        const errorData = await response.json();
41        throw new Error(errorData.message || `HTTP Error: ${response.status}`);
42      } catch (e) {
43        throw new Error(`HTTP Error: ${response.status}`);
44      }
45    }
46
47    const contentType = response.headers.get('content-type');
48    if (contentType && contentType.includes('application/json')) {
49      return await response.json();
50    } else if (contentType && contentType.includes('text/')) {
51      return await response.text();
52    } else {
53      return await response.blob();
54    }
55  } catch (error) {
56    console.error(`API request error (${endpoint}):`, error);
57
58    const apiError = new Error(error.message);
59    apiError.isNetworkError = !window.navigator.onLine;
60    apiError.endpoint = endpoint;
61    apiError.originalError = error;
62
63    throw apiError;
64  }
65}

Simple Cache System for Requests

1const apiCache = {
2  cache: new Map(),
3  ttl: 60000, // cache time-to-live in ms (1 minute)
4
5  async get(url, options = {}) {
6    const cacheKey = `${url}|${JSON.stringify(options)}`;
7    const cachedItem = this.cache.get(cacheKey);
8
9    if (cachedItem && Date.now() < cachedItem.expiry) {
10      console.log(`Using cache for: ${url}`);
11      return cachedItem.data;
12    }
13
14    console.log(`Making request for: ${url}`);
15    const response = await fetch(url, options);
16
17    if (!response.ok) {
18      throw new Error(`HTTP Error: ${response.status}`);
19    }
20
21    const data = await response.json();
22
23    if (!options.method || options.method === 'GET') {
24      this.cache.set(cacheKey, { data, expiry: Date.now() + this.ttl });
25    }
26
27    return data;
28  },
29
30  invalidate(urlPattern) {
31    for (const key of this.cache.keys()) {
32      if (key.includes(urlPattern)) {
33        this.cache.delete(key);
34      }
35    }
36  },
37
38  clear() { this.cache.clear(); }
39};

Retry Pattern — Automatic Retries After Failure

1/**
2 * Executes a function with automatic retries
3 * @param {Function} fn - Function to execute that returns a Promise
4 * @param {Object} options - Options
5 * @param {number} options.maxRetries - Maximum number of retries
6 * @param {number} options.retryDelay - Delay before retry (ms)
7 * @param {Function} options.shouldRetry - Function determining whether to retry
8 * @param {number} options.backoffFactor - Delay multiplier (backoff)
9 * @returns {Promise} - Promise with the function result or the last error
10 */
11async function withRetry(fn, options = {}) {
12  const {
13    maxRetries = 3,
14    retryDelay = 1000,
15    shouldRetry = (error) => true,
16    backoffFactor = 2
17  } = options;
18
19  let lastError;
20
21  for (let attempt = 0; attempt <= maxRetries; attempt++) {
22    try {
23      return await fn();
24    } catch (error) {
25      lastError = error;
26
27      if (attempt === maxRetries || !shouldRetry(error)) {
28        break;
29      }
30
31      const delay = retryDelay * Math.pow(backoffFactor, attempt);
32      console.log(`Attempt ${attempt + 1} failed. Retrying in ${delay}ms...`);
33      await new Promise(resolve => setTimeout(resolve, delay));
34    }
35  }
36
37  throw lastError;
38}
39
40// Example usage with Fetch API
41async function fetchFromUnstableAPI() {
42  return withRetry(
43    () => fetch('https://api.jurassic-park.com/unstable-endpoint')
44      .then(response => {
45        if (!response.ok) throw new Error(`HTTP Error: ${response.status}`);
46        return response.json();
47      }),
48    {
49      maxRetries: 5,
50      retryDelay: 500,
51      backoffFactor: 1.5,
52      shouldRetry: (error) => {
53        // Only retry for 5xx errors (server errors) or network errors
54        return error.message.includes('HTTP Error: 5') ||
55               error instanceof TypeError;
56      }
57    }
58  );
59}

Limitations and Compatibility of the Fetch API

The Fetch API is available in all modern browsers, and since version 18 it is also natively available in Node.js. However, there are some limitations and compatibility concerns:

  1. No request progress support — Fetch has no built-in progress tracking for upload/download. For such cases, XMLHttpRequest may be a better option.

  2. No native timeout — Timeouts must be implemented using AbortController.

  3. Does not reject Promises for HTTP errors — Even if the server returns an error code, the Fetch Promise will resolve. You must check

    response.ok
    .

In older browsers, you may need to use a polyfill such as

whatwg-fetch
or a library like
axios
that provides a consistent API across environments.

Alternatives to the Fetch API

Although the Fetch API is natively available, alternatives exist that may offer additional features:

  1. Axios — A popular library that provides:

    • Consistent API between browser and Node.js
    • Automatic JSON data transformation
    • Built-in XSRF protection
    • Request progress tracking
    • Built-in timeouts
    • Automatic retries
    • Request cancellation
  2. jQuery.ajax() — In older projects using jQuery

  3. XMLHttpRequest — An older, more verbose alternative that offers more control, e.g., over request progress

When to Use Fetch API vs Alternatives?

  • Fetch is ideal when:

    • Starting a new project
    • You want to minimize dependencies
    • You need simple HTTP requests
    • Working with modern browsers
  • Consider alternatives when:

    • You need more advanced features (progress, built-in retries)
    • You must support older browsers without polyfills
    • Working in an environment where Fetch does not work well
    • You already use a library that offers HTTP features

Fetch API Best Practices

  1. Always check response.ok

    1fetch(url).then(response => {
    2  if (!response.ok) throw new Error(`HTTP Error: ${response.status}`);
    3  return response.json();
    4});
  2. Set appropriate headers

    1fetch(url, {
    2  headers: {
    3    'Content-Type': 'application/json',
    4    'Accept': 'application/json'
    5  }
    6});
  3. Use AbortController for timeouts

    1const controller = new AbortController();
    2const timeout = setTimeout(() => controller.abort(), 5000);
    3
    4fetch(url, { signal: controller.signal })
    5  .then(response => /* ... */)
    6  .catch(error => /* ... */)
    7  .finally(() => clearTimeout(timeout));
  4. Be careful with body processing

    1// WRONG — attempting to use body multiple times
    2fetch(url)
    3  .then(response => {
    4    const text = response.text(); // This consumes the body!
    5    const json = response.json(); // Error — body already consumed!
    6    return /* ... */;
    7  });
    8
    9// CORRECT — clone response if you need to use it multiple times
    10fetch(url)
    11  .then(response => {
    12    const clone = response.clone();
    13    return response.text().then(text => {
    14      return clone.json().then(json => {
    15        return { text, json };
    16      });
    17    });
    18  });
  5. Use async/await for readability

    1async function fetchData() {
    2  try {
    3    const response = await fetch(url);
    4    if (!response.ok) throw new Error(`HTTP Error: ${response.status}`);
    5    const data = await response.json();
    6    return data;
    7  } catch (error) {
    8    console.error('Error:', error);
    9    throw error;
    10  }
    11}

Summary

The Fetch API is a powerful, built-in tool for making HTTP requests in JavaScript. Its main advantages are the Promise-based API, ease of use, and versatility. Although it has some limitations compared to more advanced libraries like Axios, for most use cases it is completely sufficient.

In Jurassic Park, where reliable communication between different systems is key to the safety of visitors and dinosaurs, the Fetch API enables reliable data exchange between monitoring systems, databases, and user interfaces.

In the next exercise we will look at how to combine the Fetch API with other asynchronous techniques to create even more advanced systems for managing our digital Jurassic Park.

Go to CodeWorlds