Skip to content

Async

Asynchronous programming is a method of handling operations that may take time to complete, such as network and file system operations, without blocking the execution of the rest of the program. In this module, you will learn about asynchronous methods such as callback functions, promises, and async/await in Javascript and how to use them to handle asynchronous operations.

Promises

Asynchronous code in JavaScript is typically coded with promises and callback functions.

Promises in JavaScript represent the eventual completion (or failure) of an asynchronous operation and its resulting value. A Promise is an object with three states: pending, fulfilled, or rejected. They simplify asynchronous programming by providing a more manageable alternative to callbacks. With promises, you can chain operations and handle asynchronous results in a cleaner, more linear fashion, improving code readability and error management through .then() for success scenarios and .catch() for handling errors.

const setTimeoutPromise = function(ms) {
    return new Promise(function(resolve) {
        setTimeout(resolve, ms);
    });
};

console.log("start");
setTimeoutPromise(2000).then(function() {
    console.log("end");
});

Promises are a powerful tool for handling asynchronous operations, but they can be challenging to work with due to the complexity of chaining and error handling. One of the most significant drawbacks of promises is the “Pyramid of Doom” or “Callback Hell,” which occurs when multiple asynchronous operations are nested within callbacks, leading to deeply nested code that is hard to read, maintain, and debug. The code structure resembles a pyramid, making it difficult to understand the flow and handle errors effectively.

Imagine a scenario where you need to read a file, parse its content, make an API call based on that content, and then write the response to another file. Each step is asynchronous and requires a callback. This nesting leads to a structure that’s hard to read and maintain:

fs.readFile('start.txt', 'utf8', (err, data) => {
    if (err) throw err;
    parseData(data, (err, parsed) => {
        if (err) throw err;
        apiCall(parsed, (err, response) => {
            if (err) throw err;
            fs.writeFile('output.txt', response, (err) => {
                if (err) throw err;
                console.log('The process is complete.');
            });
        });
    });
});

This is a simplified example of callback hell, illustrating how quickly asynchronous operations can become tangled and difficult to follow.

Async/await

To address these issues, JavaScript introduced async/await, a more intuitive and readable way to handle asynchronous operations.

To use async/await in JavaScript, start by declaring a function with async before the function keyword. Inside this function, you can use await to pause execution until a promise resolves, simplifying the handling of asynchronous operations.

const setTimeoutPromise = (ms) =>
    new Promise((resolve) => setTimeout(resolve, ms));

async function test() {
    console.log("start");
    let t = await setTimeoutPromise(2000);
    console.log("end");
}

test();

Several promise objects can optionally be awaited with all:

const setTimeoutPromise = (ms) =>
    new Promise((resolve) => setTimeout(resolve, ms));

async function test() {
    console.log("start");
    const t1 = setTimeoutPromise(2000);
    const t2 = setTimeoutPromise(2000);
    await Promise.all([t1, t2]);
    console.log("end");
}

test();

Async/await in JavaScript simplifies handling asynchronous operations, making code easier to read and debug by allowing asynchronous code to be written in a synchronous-like manner. This approach reduces the complexity associated with nested callbacks and promise chains, leading to cleaner, more intuitive code. Async/await also streamlines error handling, enabling the use of traditional try/catch blocks for asynchronous code, which enhances code clarity and error management practices.

Promise Methods

JavaScript provides several methods for working with multiple promises simultaneously:

Promise.all()

Waits for all promises to resolve. If any promise rejects, the entire operation fails.

const promise1 = fetch('https://api.example.com/data1');
const promise2 = fetch('https://api.example.com/data2');
const promise3 = fetch('https://api.example.com/data3');

try {
    const [data1, data2, data3] = await Promise.all([promise1, promise2, promise3]);
    console.log('All data loaded successfully');
} catch (error) {
    console.error('One or more requests failed:', error);
}

Promise.allSettled()

Waits for all promises to complete (either resolve or reject), and returns the results of all promises regardless of success or failure.

const results = await Promise.allSettled([
    fetch('https://api.example.com/data1'),
    fetch('https://api.example.com/data2'),
    fetch('https://api.example.com/invalid-url')
]);

results.forEach((result, index) => {
    if (result.status === 'fulfilled') {
        console.log(`Promise ${index} succeeded:`, result.value);
    } else {
        console.log(`Promise ${index} failed:`, result.reason);
    }
});

Promise.race()

Returns the first promise to settle (either resolve or reject).

const timeout = new Promise((_, reject) =>
    setTimeout(() => reject(new Error('Timeout')), 5000)
);

const dataFetch = fetch('https://api.example.com/data');

try {
    const result = await Promise.race([dataFetch, timeout]);
    console.log('Data received before timeout');
} catch (error) {
    console.error('Request timed out or failed:', error);
}

Promise.any()

Returns the first promise to successfully resolve. If all promises reject, it returns an AggregateError.

// Try multiple backup servers
const backupServers = [
    fetch('https://server1.example.com/data'),
    fetch('https://server2.example.com/data'),
    fetch('https://server3.example.com/data')
];

try {
    const firstSuccess = await Promise.any(backupServers);
    console.log('Got data from first available server');
} catch (error) {
    console.error('All servers failed:', error);
}

AJAX and Fetch API

AJAX (Asynchronous JavaScript and XML) is the old term for the functionality in browsers that allows making HTTP calls.

In older browsers, this was done using the XMLHttpRequest object, but in newer browsers, it is done using the fetch API. With this API, you can make all types of HTTP calls relatively simply.

Basic fetch usage

The fetch API provides a modern way to make HTTP requests in JavaScript. It returns a Promise that resolves to the Response object representing the response to the request.

Basic GET request:

fetch('https://api.example.com/data')
    .then(response => {
        if (!response.ok) {
            throw new Error('Network response was not ok');
        }
        return response.json();
    })
    .then(data => {
        console.log(data);
    })
    .catch(error => {
        console.error('There was a problem with the fetch operation:', error);
    });

In this example:

  • fetch('https://api.example.com/data') initiates a GET request to the specified URL
  • The first .then() handles the response. If the response is not OK (status is not in the range 200-299), it throws an error
  • response.json() parses the JSON body of the response
  • The second .then() processes the parsed JSON data
  • The .catch() handles any errors that occur during the fetch operation

Fetch with different HTTP methods

POST request:

fetch('https://api.example.com/users', {
    method: 'POST',
    headers: {
        'Content-Type': 'application/json'
    },
    body: JSON.stringify({
        name: 'John Doe',
        email: 'john@example.com'
    })
})
    .then(response => response.json())
    .then(data => console.log('User created:', data))
    .catch(error => console.error('Error:', error));

PUT request:

fetch('https://api.example.com/users/123', {
    method: 'PUT',
    headers: {
        'Content-Type': 'application/json'
    },
    body: JSON.stringify({
        name: 'Jane Doe',
        email: 'jane@example.com'
    })
})
    .then(response => response.json())
    .then(data => console.log('User updated:', data))
    .catch(error => console.error('Error:', error));

DELETE request:

fetch('https://api.example.com/users/123', {
    method: 'DELETE'
})
    .then(response => {
        if (response.ok) {
            console.log('User deleted successfully');
        }
    })
    .catch(error => console.error('Error:', error));

Aborting fetch requests

You can cancel fetch requests using AbortController. This is useful for cleaning up requests when components unmount or when a new request supersedes an old one.

const controller = new AbortController();
const signal = controller.signal;

// Start the fetch with abort signal
fetch('https://api.example.com/data', { signal })
    .then(response => response.json())
    .then(data => console.log(data))
    .catch(error => {
        if (error.name === 'AbortError') {
            console.log('Fetch aborted');
        } else {
            console.error('Fetch error:', error);
        }
    });

// Abort the fetch after 5 seconds
setTimeout(() => controller.abort(), 5000);

When to use AbortController

Use AbortController to:

  • Cancel requests when user navigates away
  • Implement request timeouts
  • Cancel outdated requests when new ones are made (e.g., search autocomplete)
  • Clean up when React/Vue components unmount

Example without async/await

index.html

<!DOCTYPE html>
<html lang="en">
    <head>
        <meta charset="UTF-8" />
        <meta name="viewport" content="width=device-width, initial-scale=1.0" />
        <title>Document</title>
    </head>
    <body>
        <button id="knap">Knap</button>
        <script src="app.js"></script>
    </body>
</html>

app.js

(function () {
    // fetch all municipalities in DK when the button is clicked
    let button = document.querySelector("#knap");
    button.onclick = function () {
        fetch("https://dawa.aws.dk/kommuner/")
            .then((response) => {
                response.json().then((json) => {
                    console.log(json);
                });
            })
            .catch((error) => {
                console.log(error);
            });
    };
})();

Example using async/await

index.html

<!DOCTYPE html>
<html lang="en">
    <head>
        <meta charset="UTF-8" />
        <meta name="viewport" content="width=device-width, initial-scale=1.0" />
        <title>Document</title>
    </head>
    <body>
        <button id="knap">Knap</button>
        <script src="app.js"></script>
    </body>
</html>

app.js

(function () {
    // fetch all municipalities in DK when the button is clicked
    let button = document.querySelector("#knap");
    button.onclick = async function () {
        try {
            let response = await fetch("https://dawa.aws.dk/kommuner/");
            let json = await response.json();
            console.log(json);
        } catch (error) {
            console.log(error);
        }
    };
})();

Using fetch in Node.js

In Node.js, you can use the node-fetch package to make HTTP calls. This package is (now) included in Node.js by default. Here is an example of how to use fetch in Node.js. Here with promises:

(function() {
    fetch("https://api.dataforsyningen.dk/kommuner")
        .then(response => response.json())
        .then(data => console.log(data))
        .catch(error => console.error("Fejl:", error));
})();

and here with async/await:

(async function() {
    const response = await fetch("https://api.dataforsyningen.dk/kommuner");
    const data = await response.json();
    console.log(data);
})();

The code is wrapped in an immediately invoked async function expression (IIFE). This pattern allows the use of await within the function, enabling asynchronous operations to be handled more cleanly.

Within the async function, a try…catch block is used to handle potential errors that may occur during the fetch operation. The fetch function is called with the URL https://api.dataforsyningen.dk/kommuner, which initiates an HTTP GET request to the specified endpoint. The await keyword ensures that the code waits for the fetch operation to complete before proceeding.

Once the response is received, the response.json() method is called to parse the JSON data from the response. This operation is also awaited, ensuring that the code waits for the JSON parsing to complete. The parsed data is then logged to the console using console.log(data).

Best Practices

Error handling:

  • Always check response.ok before parsing the response
  • Use try/catch blocks with async/await
  • Handle network errors separately from HTTP errors
    async function fetchData(url) {
        try {
            const response = await fetch(url);
    
            if (!response.ok) {
                throw new Error(`HTTP error! status: ${response.status}`);
            }
    
            const data = await response.json();
            return data;
        } catch (error) {
            if (error.name === 'TypeError') {
                console.error('Network error:', error);
            } else {
                console.error('Fetch error:', error);
            }
            throw error;
        }
    }
    

Performance:

  • Use Promise.all() for parallel requests when order doesn’t matter
  • Use Promise.allSettled() when you want all results regardless of failures
  • Implement timeouts with Promise.race() or AbortController
  • Cache responses when appropriate

Security:

  • Validate and sanitize data from API responses
  • Use HTTPS for API calls
  • Be careful with CORS - understand same-origin policy
  • Never expose API keys in client-side code

Assignment(s)


Libraries

There are several libraries that can make it easier to make HTTP calls - for example, jQuery and Axios. You might want to check out https://github.com/devcronberg/nodedemo, which among other things shows the use of Axios in different types of applications.

Test JSON server

If you want to test your AJAX calls, you can use a JSON server. This is a server that can serve JSON files. You can install it by creating a new project and then:

npm init -y
npm install json-server

Then add a db.json file with some content (see full example in original text).

Then you can start the server with:

npx json-server db.json

This will start a server on http://localhost:3000 with the data from the db.json file.

Summary

Asynchronous programming is essential for modern JavaScript development, enabling non-blocking operations for network requests, file I/O, and other time-consuming tasks.

Key Takeaways

Promises:

  • Represent eventual completion or failure of async operations
  • Three states: pending, fulfilled, rejected
  • Use .then() for success, .catch() for errors
  • Avoid callback hell by using promises

Async/Await:

  • Syntactic sugar over promises
  • Makes async code look synchronous
  • Use async keyword before function
  • Use await keyword before promises
  • Requires try/catch for error handling
  • Cleaner and more readable than promise chains

Promise Methods:

  • Promise.all(): Wait for all promises (fails if any fails)
  • Promise.allSettled(): Wait for all promises (returns all results)
  • Promise.race(): Returns first promise to settle
  • Promise.any(): Returns first promise to succeed

Fetch API:

  • Modern way to make HTTP requests
  • Returns promises
  • Supports all HTTP methods (GET, POST, PUT, DELETE, etc.)
  • Use response.ok to check for success
  • Parse response with .json(), .text(), etc.
  • Works in browsers and Node.js

Advanced Features:

  • AbortController: Cancel fetch requests
  • Request options: Headers, body, method, etc.
  • Error handling: Network vs HTTP errors
  • Timeouts: Use Promise.race() or AbortController

Best Practices:

  • Always handle errors with try/catch or .catch()
  • Check response.ok before parsing
  • Use parallel requests with Promise.all() when possible
  • Implement timeouts for long-running requests
  • Cancel requests when they’re no longer needed
  • Validate API responses before using them

Understanding async programming enables you to build responsive applications that handle real-world operations like API calls, file uploads, and real-time updates effectively.