• ๐Ÿ”– [[Asynchronous#|Parallel vs Async]]
  • ๐Ÿ”– [[Asynchronous#|Callbacks]]
  • ๐Ÿ”– [[Asynchronous#|Thunks]]
  • ๐Ÿ”– XMLHttpRequest
  • ๐Ÿ”– Promises
  • ๐Ÿ”– Fetch
  • ๐Ÿ”– Await
  • ๐Ÿ”– [[Asynchronous#|Generators/Coroutines]]
  • ๐Ÿ”– [[Asynchronous#|Event Reactive (observables)]]
  • ๐Ÿ”– [[Asynchronous#|CSP (channel-oriented concurrency)]]

Parallel vs Async

Parallelism

Parallelism is a concept in computing where multiple tasks or processes are executed simultaneously, potentially taking advantage of multiple processors or cores in a computer system. Itโ€™s a way to improve the performance and efficiency of a system by breaking down a problem into smaller tasks that can be executed concurrently

Concurrency vs. Parallelism:

Concurrency and parallelism are related but different concepts.

  • Concurrency is about managing multiple tasks and making progress on them, even if they are not executed simultaneously.
  • Parallelism, on the other hand, involves executing multiple tasks simultaneously to improve overall performance.

Asynchronous

Asynchronous execution in JavaScript is a fundamental concept. It allows you to perform tasks without blocking the main thread. This is crucial for non-blocking I/O operations like fetching data from an API, reading a file, or making network requests. Asynchronous code often involves callbacks, Promises, or the more modern async/await syntax.

Callback

What Are Callbacks?

In JavaScript, a callback is a function that is passed as an argument to another function. The primary purpose of a callback is to be executed later, after the completion of a particular operation or task. Callbacks are commonly used for asynchronous operations, such as handling events, making HTTP requests, or reading files.

Asynchronous Operations and Callbacks:

Callbacks are often associated with asynchronous operations, where a task takes some time to complete, and you donโ€™t want to block the main thread of execution. Instead of waiting for the operation to finish, you pass a callback function to handle the result when itโ€™s ready.

Callback Hell

Most people think of the call back hell has something to do with the indentation and the nesting but really it's not

setTimeout(function () {
  console.log("one")
  setTimeout(function () {
    console.log("two")
    setTimeout(function () {
      console.log("three")
    }, 1000)
  }, 1000)
}, 1000)

Cause we can rewrite the program using continuation passing style (CPS) and still have a callback hell

function one(callback) {
  setTimeout(function () {
    console.log("one")
    callback()
  }, 1000)
}
 
function two(callback) {
  setTimeout(function () {
    console.log("two")
    callback()
  }, 1000)
}
 
function three() {
  setTimeout(function () {
    console.log("three")
  }, 1000)
}
 
one(function () {
  two(function () {
    three()
  })
})

Major Issue with call back

Callbacks can lead to callback hell, also known as the "pyramid of doom," when multiple asynchronous operations are nested inside one another. This can make code hard to read and maintain. To address this issue, modern JavaScript introduced Promises and async/await, which provide more structured and readable ways to handle asynchronous code.

Inversion of Control (IoC):

IoC refers to the practice of shifting control over certain aspects of a program from the program itself to an external component or framework. In other words, it involves allowing an external entity to control or manage parts of a programโ€™s behavior.

Callbacks:

Callbacks are a mechanism for handling asynchronous operations in JavaScript. They allow you to specify a function (callback) to be executed once an operation is complete. In terms of IoC, callbacks can be seen as a form of Inversion of Control because the execution of the callback is controlled by the asynchronous operation or an external event. The main program hands over control to the callback function.

Promises:

Promises provide a more structured way to work with asynchronous operations. They encapsulate the result or error of an asynchronous task and allow you to attach .then() and .catch() handlers to specify what should happen when the task is complete or encounters an error. we are in a complete control of that paradigm. we have brought many sanity to this control.

With promises, we simply uninvert the inversion of control problem, promises were designed to retain the control we usually have in our code.

Thunks

A thunk is a function that encapsulates an action or a computation that can be deferred or delayed.

In the context of Redux and state management in JavaScript, a thunk is often used to represent asynchronous actions as functions. These functions return another function that can dispatch actions or perform asynchronous operations.

Thunks are typically used in conjunction with middleware like Redux Thunk to manage asynchronous side effects in a Redux application.

XMLHttpRequest

XMLHttpRequest is an API that allows you to make HTTP requests from a web page or a web application. It has been a fundamental part of web development for many years,

It is recommended to use newer APIs like the Fetch API are now recommended for making HTTP requests.

Creation of XHR Object:

Creating our XMLHTTP Instance

To make an HTTP request using XMLHttpRequest, you first need to create an instance of the XMLHttpRequest object.

const xhr = new XMLHttpRequest()

Configuring the Request:

You configure the request by specifying the HTTP method (GET, POST, etc.) and the URL you want to request. You can also set request headers if needed.

xhr.open("GET", "https://example.com/api/data", true)
// true for asynchronous, false for synchronous

The third parameter (true) specifies whether the request should be asynchronous (true) or synchronous (false). Itโ€™s highly recommended to use asynchronous requests (true) to prevent blocking the main thread.

Configuring Headers

Yup you can set one or multiple headers as needed using the setRequestHeader

// Set multiple request headers
xhr.setRequestHeader("Content-Type", "application/json") // Content-Type header
xhr.setRequestHeader("Authorization", "Bearer yourAccessToken") // Custom authorization header
xhr.setRequestHeader("Custom-Header", "Custom-Value") // Another custom header

Sending the Request:

To initiate the request, you use the send() method. If youโ€™re making a POST request and need to send data, you can pass it as an argument to send().

xhr.send()
const data = { key1: "value1", key2: "value2" }
const jsonData = JSON.stringify(data)
xhr.send(jsonData) // Send the JSON data as the request body

Handling Responses: (onload on errors)

You set up event listeners to handle the response when it arrives. Common events include load, error, and progress.

xhr.onload = function () {
  if (xhr.status === 200) {
    // Handle successful response
    console.log(xhr.responseText)
  } else {
    // Handle other HTTP status codes
    console.error("HTTP error:", xhr.status, xhr.statusText)
  }
}
 
xhr.onerror = function () {
  // Handle network errors
  console.error("Network error")
}

The onload event is triggered when the request completes successfully (HTTP status code 200). The onerror event is triggered for network errors or HTTP errors.

Synchronous vs. Asynchronous:

  • ๐Ÿ”ฅ XHR requests can be made in either synchronous or asynchronous mode:
    • โžก๏ธ Synchronous (Blocking):
      • ๐Ÿ”– Setting the third parameter of xhr.open() to false makes the request synchronous. This means that the JavaScript code execution is blocked until the request completes. This approach is strongly discouraged because it can freeze the user interface and negatively affect the user experience.
    • โžก๏ธ Asynchronous (Recommended):
      • ๐Ÿ”– Setting the third parameter to true (or omitting it, as it defaults to true) makes the request asynchronous. In this mode, the request is sent to the server without blocking the rest of your code. You provide callback functions to handle the response when it arrives.

Making a Simple GET Request:

const xhr = new XMLHttpRequest()
xhr.open("GET", "https://example.com/api/data", true)
xhr.onload = function () {
  if (xhr.status === 200) {
    // Handle successful response
    console.log(xhr.responseText)
  } else {
    // Handle HTTP errors
    console.error("HTTP error:", xhr.status, xhr.statusText)
  }
}
xhr.onerror = function () {
  // Handle network errors
  console.error("Network error")
}
xhr.send()

ON READY STATE CHANGE EVENT LISTENER

Sometimes people attach an onReadyStateChange event listener to the xhr object and it would give somethin like this

xhr.onreadystatechange = function () {
  if (xhr.readyState === 4) {
    // The request is complete (readyState 4)
    if (xhr.status === 200) {
      // Handle a successful response
      console.log(xhr.responseText)
    } else {
      // Handle HTTP errors
      console.error("HTTP error:", xhr.status, xhr.statusText)
    }
  }
}

In this code, we check if xhr.readyState is equal to 4, which indicates that the request is complete. If xhr.status is 200, we handle a successful response; otherwise, we handle HTTP errors.

This approach allows you to handle different stages of the request, such as when the request is in progress ( readyState 1, 2, 3) and when it's complete (readyState 4). It gives you more fine-grained control over handling the response and errors.

const xhr = new XMLHttpRequest()
xhr.open("GET", "https://example.com/api/data", true)
xhr.setRequestHeader("Content-Type", "application/json")
xhr.onreadystatechange = function () {
  if (xhr.readyState === 4) {
    if (xhr.status === 200) {
      console.log(xhr.responseText) // Handle successful response
    } else {
      console.error("HTTP error:", xhr.status, xhr.statusText) // Handle HTTP errors
    }
  }
}
xhr.send()

Making a simple Post Request

const xhr = new XMLHttpRequest()
xhr.open("POST", "https://example.com/api/postEndpoint", true)
xhr.setRequestHeader("Content-Type", "application/json") // Set the content type
const data = {
  key1: "value1",
  key2: "value2",
}
const jsonData = JSON.stringify(data)
 
xhr.onload = function () {
  if (xhr.status === 200) {
    console.log(xhr.responseText) // Handle successful response
  } else {
    console.error("HTTP error:", xhr.status, xhr.statusText) // Handle HTTP errors
  }
}
 
xhr.onerror = function () {
  console.error("Network error") // Handle network errors
}
 
xhr.send(jsonData) // Send the JSON data as the request body

Creating a Promise based version of XMLHttpRequest

function makeHttpRequest(method, url) {
  return new Promise((resolve, reject) => {
    const xhr = new XMLHttpRequest()
 
    xhr.open(method, url, true)
 
    xhr.onload = () => {
      if (xhr.status >= 200 && xhr.status < 300) {
        // Resolve the Promise with the response text
        resolve(xhr.responseText)
      } else {
        // Reject the Promise with an error message
        reject(`HTTP Error: ${xhr.status} - ${xhr.statusText}`)
      }
    }
 
    xhr.onerror = () => {
      // Reject the Promise with a network error message
      reject("Network Error")
    }
 
    // Send the request
    xhr.send()
  })
}
 
// Example usage:
makeHttpRequest("GET", "https://jsonplaceholder.typicode.com/posts/1")
  .then((response) => {
    console.log("Response:", response)
  })
  .catch((error) => {
    console.error("Error:", error)
  })

Promises

Promises are objects that represents a time independent eventual completion or failure of an asynchronous operation. They have three states: pending, resolved (fulfilled), and rejected.

Introducing the readability enhancer - Promises

Overview

What is it ?

In JavaScript, a Promise is a built-in object that represents a placeholder for a value that may not be available yet but will be resolved at some point in the future. It is used for handling asynchronous operations in a more elegant and organized way, making it easier to write and manage asynchronous code.

Promises were later standardized in ECMAScript 6 (ES6) through the Promises/A+ specification, making them a part of the core JavaScript language. They have since become a fundamental feature of modern JavaScript development, and their ease of use and clean syntax have significantly improved the way developers handle asynchronous operations.

How do they work ?

Special objects built into JavaScript that get returned immediately when we make a call to a web browser API/feature (e.g. fetch) thatโ€™s set up to return promises (not all are)

  • ๐Ÿ’ก Promises act as a placeholder for the data we hope to get back from the web browser featureโ€™s background work
  • ๐Ÿ’ก We also attach the functionality we want to defer running until that background work is done (using the built in .then method)
  • ๐Ÿ’ก Promise objects will automatically trigger that functionality to run
  • ๐Ÿ’ก The value returned from the web browser featureโ€™s work (e.g. the returned data from the server using fetch) will be that functionโ€™s input/argument

    Promises in JavaScript work under the hood by managing the state of asynchronous operations and providing a structured way to handle their results or errors. Promises are implemented as a combination of JavaScript code and the event loop, which is the core of JavaScriptโ€™s concurrency model.

Why were they made in the first place ?

Promise and trust notion.

Another way of thinking bout Promises is that it's simply a callback manager, it is a pattern for managing our callbacks in a trustable manner.

  • ๐Ÿ”ฅ Only Resolved once
  • ๐Ÿ”ฅ Either success or error
  • ๐Ÿ”ฅ Messages passed/kept
  • ๐Ÿ”ฅ Exceptions become errors
  • ๐Ÿ”ฅ Immutable once resolved

The advantages of promises.

Fixing the callback hell

Callback hell occurs when multiple asynchronous operations are nested deeply inside one another, leading to a complex and hard-to-maintain code structure. Promises provide a more structured and readable way to handle asynchronous code, reducing the likelihood of callback hell.

Error handling

In callback-based asynchronous code, error handling can become challenging because each callback must include error handling logic. Promises centralize error handling with the .catch() method, making it easier to handle and propagate errors consistently throughout the code.

Guaranteed Resolution or Rejection:

Promises ensure that an asynchronous operation will either resolve (succeed) or reject (fail) once and only once. This guarantee is important for reliability and consistency in asynchronous code. Callbacks can be called multiple times, which can lead to unexpected behavior.

Readability and Maintainability:

Promises improve code readability by allowing you to chain .then() methods in a sequential and organized manner, making it clear how asynchronous operations are coordinated.

Composition:

Promises support composition, where you can combine multiple asynchronous operations into a single flow of control. This is valuable for orchestrating complex asynchronous tasks.

Avoiding Callback Pyramid:

Promises help avoid the callback pyramid structure by providing a more linear and visually organized way to express asynchronous code.

Better Debugging:

Promises offer better debugging capabilities, as you can set breakpoints and inspect the flow of control more easily than with deeply nested callbacks.

Eventual Consistency:

Promises help ensure that code relying on asynchronous operations remains consistent and predictable, reducing race conditions and timing issues.

Thenables ar Promises

The JavaScript ecosystem had made multiple Promise implementations long before it became part of the language. Despite being represented differently internally, at the minimum, all Promise-like objects implement the Thenable interface. A thenable implements the .then() method, which is called with two callbacks: one for when the promise is fulfilled, one for when itโ€™s rejected. Promises are thenables as well.

A "thenable" is an object that has a .then() method. It doesn't have to be a Promise itself, but it behaves like one in the sense that you can attach .then() callbacks to it.

const thenable = {
  then(resolve, reject) {
    setTimeout(() => {
      resolve("This is a thenable!")
    }, 1000)
  },
}
 
thenable.then(
  (result) => {
    console.log(result)
  },
  (error) => {
    console.error(error)
  },
)

In this case, thenable is not a Promise, but it has a .then() method, allowing us to use .then() to handle its asynchronous operation.

So, a โ€œthenableโ€ is any object that conforms to the expected behavior of having a .then() method, which allows it to be used in Promise-like asynchronous workflows. Understanding โ€œthenablesโ€ can be useful when working with Promises and related asynchronous patterns in JavaScript.

Control flow

Promises are objects that represent the eventual completion or failure of an asynchronous operation. They have three states: pending, resolved (fulfilled), and rejected. Promises are used to manage the flow of asynchronous code, making it more organized and readable.

const myPromise = new Promise((resolve, reject) => {
  // Asynchronous operation here
  // Call resolve(result) when successful
  // Call reject(error) if it fails
})

Creating a promise instance

To Create a promise you simply create an instance from the Promise class and pass it your executor function.

Yes, the executor function is a must and yes it gets executed as soon as you create your promise.

const promiseA = new Promise(ExecutorFunction)
const myPromise = new Promise((resolve, reject) => {
  // The executor function takes two argument, reject and resolve
})

then(), finally(), catch()

Promise.prototype.then()

Syntax
then(onFulfilled)
then(onFulfilled, onRejected)
Usage

The then() method of Promise instances takes up to two arguments: callback functions for the fulfilled and rejected cases of the Promise. It immediately returns an equivalent Promise object, allowing you to chain calls to other promise methods.

const promise1 = new Promise((resolve, reject) => {
  resolve("Success!")
})
 
promise1.then((value) => {
  console.log(value)
  // Expected output: "Success!"
})
const p1 = new Promise((resolve, reject) => {
  resolve("Success!")
  // or
  // reject(new Error("Error!"));
})
 
p1.then(
  (value) => {
    console.log(value) // Success!
  },
  (reason) => {
    console.error(reason) // Error!
  },
)
Having a non-function as either parameter
Promise.resolve(1).then(2).then(console.log) // 1
Promise.reject(1).then(2, 2).then(console.log, console.log) // 1

Promise.prototype.catch()

The catch() method of Promise instances schedules a function to be called when the promise is rejected. It immediately returns an equivalent Promise object, allowing you to chain calls to other promise methods. It is a shortcut for Promise.prototype.then(undefined, onRejected).

const promise1 = new Promise((resolve, reject) => {
  throw new Error("Uh-oh!")
})
 
promise1.catch((error) => {
  console.error(error)
})
// Expected output: Error: Uh-oh!

If a promise becomes rejected, and there are no rejection handlers to call (a handler can be attached through any of then(), catch(), or finally()), then the rejection event is surfaced by the host. In the browser, this results in an unhandledrejection event.

โœ…catch() internally calls then() on the object upon which it was called, passing undefined and onRejected as arguments. The value of that call is directly returned. This is observable if you wrap the methods.๐Ÿ˜‚

Gothas Throwing Errors.

Throwing an error will call the catch() method most of the time:

const p1 = new Promise((resolve, reject) => {
  throw new Error("Uh-oh!")
})
 
p1.catch((e) => {
  console.error(e) // "Uh-oh!"
})

Errors thrown inside asynchronous functions will act like uncaught errors:

const p2 = new Promise((resolve, reject) => {
  setTimeout(() => {
    throw new Error("Uncaught Exception!")
  }, 1000)
})
 
p2.catch((e) => {
  console.error(e) // This is never called
})

Errors thrown after resolve is called will be silenced:

const p3 = new Promise((resolve, reject) => {
  resolve()
  throw new Error("Silenced Exception!")
})
 
p3.catch((e) => {
  console.error(e) // This is never called
})

Catch is never called if the promise is fulfilled.

// Create a promise which would not call onReject
const p1 = Promise.resolve("calling next")
 
const p2 = p1.catch((reason) => {
  // This is never called
  console.error("catch p1!")
  console.error(reason)
})
 
p2.then(
  (value) => {
    console.log("next promise's onFulfilled")
    console.log(value) // calling next
  },
  (reason) => {
    console.log("next promise's onRejected")
    console.log(reason)
  },
)

Promise.prototype.finally()

The finally() method of Promise instances schedules a function to be called when the promise is settled (either fulfilled or rejected). It immediately returns an equivalent Promise object, allowing you to chain calls to other promise methods.

This lets you avoid duplicating code in both the promiseโ€™s then() and catch() handlers.

function checkMail() {
  return new Promise((resolve, reject) => {
    if (Math.random() > 0.5) {
      resolve("Mail has arrived")
    } else {
      reject(new Error("Failed to arrive"))
    }
  })
}
 
checkMail()
  .then((mail) => {
    console.log(mail)
  })
  .catch((err) => {
    console.error(err)
  })
  .finally(() => {
    console.log("Experiment completed")
  })

The finally() method is very similar to calling then(onFinally, onFinally). However, there are a couple of differences:

Example using finaly

let isLoading = true
 
fetch(myRequest)
  .then((response) => {
    const contentType = response.headers.get("content-type")
    if (contentType && contentType.includes("application/json")) {
      return response.json()
    }
    throw new TypeError("Oops, we haven't got JSON!")
  })
  .then((json) => {
    /* process your JSON further */
  })
  .catch((error) => {
    console.error(error) // this line can also throw, e.g. when console = {}
  })
  .finally(() => {
    isLoading = false
  })

Chaining Promises for Sequential Execution

Promises can be chained using .then() to ensure sequential execution of asynchronous tasks. Each .then() block executes only when the previous one resolves.

asyncTask1()
  .then(() => asyncTask2())
  .then(() => asyncTask3())
  .then(() => {
    // All tasks have completed sequentially
  })
  .catch((error) => {
    // Handle errors
  })

Parallel Execution with Promise.all()

Promise.all() allows you to execute multiple asynchronous tasks concurrently and wait for all of them to complete before proceeding.

const promises = [asyncTask1(), asyncTask2(), asyncTask3()]
Promise.all(promises)
  .then((results) => {
    // All tasks have completed in parallel, and results are available
  })
  .catch((error) => {
    // Handle errors from any of the tasks
  })

Error Handling with Promises

Promises provide consistent error handling using .catch(). You can place a single .catch() handler at the end of a chain to handle any errors that occur in any step.

asyncTask1()
  .then(() => asyncTask2())
  .then(() => asyncTask3())
  .then(() => {
    // All tasks have completed sequentially
  })
  .catch((error) => {
    // Handle errors from any step
  })

Conditional Control Flow

Use conditional statements within your promise chain to control whether specific tasks should execute based on certain conditions.

asyncTask1()
  .then(() => {
    if (condition) {
      return asyncTask2()
    }
  })
  .then(() => asyncTask3())
  .then(() => {
    // Task 2 executed conditionally
  })
  .catch((error) => {
    // Handle errors
  })

Returning Promises in .then()

Each .then() can return a new Promise, enabling complex control flow scenarios like branching or dynamically generating promises based on previous results.

asyncTask1()
  .then((result) => {
    if (result === "condition") {
      return asyncTask2()
    } else {
      return asyncTask3()
    }
  })
  .then(() => {
    // Either asyncTask2 or asyncTask3 executed based on the condition
  })
  .catch((error) => {
    // Handle errors
  })

Using promises.

Promises provide a more structured way of handling asynchronous operations. A Promise is an object representing the eventual completion or failure of an asynchronous operation. It has methods like then() and catch() to handle success and error cases respectively.

Creating a promise

A Promise is an object representing the eventual completion or failure of an asynchronous operation. Since most people are consumers of already-created promises

Creating a promise

const myPromise = new Promise((resolve, reject) => {
  // Do some asynchronous operation
  // If successful, call resolve
  // If error, call reject
 
  if (/* operation successful */) {
   // Resolve the promise with the result
	   resolve(result);
   } else {
   // Reject the promise with an error
   reject(error);
   }
});
 
/* Consume the Promise To consume the promise and access the result or handle errors, you use the **then()** and **catch()** methods. The `then()` method is called when the promise is fulfilled (resolved), and the `catch()` method is called when the promise is rejected. */
 
myPromise
  .then(result => {
    // Handle the fulfilled promise (success)
    console.log(result);
  })
  .catch(error => {
    // Handle the rejected promise (error)
    console.error(error);
  });

Creating a promise that wrap normal piece of codes.

A promise version of a setTimeout
// Creating our promise version of setTimeout
function setTimeoutPromise(duration) {
  // Create a promise instance to have access
  // then and catch
  return new Promise((resolve, reject) => {
    //  Passw the reference of resolve which is our then
    // to our set timeout method. and the duration which is
    //simply a parameter of our function.
    setTimeout(resolve, duration)
  })
}
 
//Calling our method with a 1000 duration param
// Using then which is same as the reference to our resolve
setTimeoutPromise(1000).then(() => {
  console.log("Boom CHakalaka")
})
 
// You can chain multiple promises too !
setTimeoutPromise(1000)
  .then(() => {
    console.log("Boom ")
    return setTimeoutPromise(2000)
  })
 
  .then(() => {
    console.log("Chakalaka")
 
    return setTimeoutPromise(1000)
  })
 
  .then(() => {
    console.log("BOOM BOOM CHAKALAKA")
  })
A promise an event listener
// Our element
const button = document.querySelector("button")
 
//Creating our promise version of event listener
function addEventListenerPromise(element, method) {
  //To have access to then and catch, Must create an instance or promise
  return new Promise((resolve, reject) => {
    //Instead of taking a call back function, our event listener simply takes a reference to the resolve method.(which is simply our then.)
    element.addEventListener(method, resolve)
  })
}
 
//Using our promise version of event listener.
addEventListenerPromise(button, "click").then((e) => {
  console.log("Clicked")
  console.log(e) // the e gets basses as argument to our funciton reference(resolve)
})

Here our event will only run once and wont run again

The reason your promise-based event listener is running only once is because the resolve function is invoked only once when the event occurs. After that, the promise is considered fulfilled, and subsequent invocations of the event will not trigger the resolve function again.

function promiseEventListener(element, eventType) {
  return new Promise((resolve, reject) => {
    const e = document.querySelector(element)
    if (e !== null) {
      e.addEventListener(eventType, resolve)
    } else {
      reject("Unknown Element")
    }
  })
}
 
// Usage example
function printNice() {
  console.log("Bonjour paris")
}
 
function listenForClick() {
  promiseEventListener("body", "click")
    .then(() => {
      printNice()
      listenForClick() // Listen again for the next click
    })
    .catch((e) => {
      console.log(e)
    })
}
 
listenForClick()

Chaining promises

A common need is to execute two or more asynchronous operations back to back, where each subsequent operation starts when the previous operation succeeds, with the result from the previous step. In the old days, doing several asynchronous operations in a row would lead to the classic callback pyramid of doom:

With promises, we accomplish this by creating a promise chain. The API design of promises makes this great, because callbacks are attached to the returned promise object, instead of being passed into a function.

Chaining Promises (Optional) Promises can be chained together to perform a series of asynchronous operations in a sequential manner. You can use the then() method to chain multiple promises.

myPromise
  .then((result) => {
    // Handle the first promise
    return anotherPromise(result)
    // Return another promise
  })
  .then((anotherResult) => {
    // Handle the second promise
    console.log(anotherResult)
  })
  .catch((error) => {
    // Handle any errors in the chain
    console.error(error)
  })

By chaining promises, you can ensure that each asynchronous operation is executed one after the other, and handle any errors that occur along the way.

With this pattern, you can create longer chains of processing, where each promise represents the completion of one asynchronous step in the chain. In addition, the arguments to then are optional, and catch(failureCallback) is short for then(null, failureCallback) โ€” so if your error handling code is the same for all steps, you can attach it to the end of the chain:

function asyncFunction() {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      resolve("Data")
      // or reject(new Error('Something went wrong'));
    }, 1000)
  })
}
asyncFunction()
  .then((data) => {
    console.log(data)
  })
  .catch((error) => {
    console.error(error)
  })

Always Return Results

Warning Important: Always return results, otherwise callbacks won't catch the result of a previous promise

doSomething()
  .then((url) => {
    // I forgot to return this
    fetch(url)
  })
  .then((result) => {
    // result is undefined, because nothing is returned from
    // the previous handler.
    // There's no way to know the return value of the fetch()
    // call anymore, or whether it succeeded at all.
  })

This may be worse if you have race conditions: if the promise from the last handler is not returned, the next then handler will be called early, and any value it reads may be incomplete.

const listOfIngredients = []
 
doSomething()
  .then((url) => {
    // I forgot to return this
    fetch(url)
      .then((res) => res.json())
      .then((data) => {
        listOfIngredients.push(data)
      })
  })
  .then(() => {
    console.log(listOfIngredients)
    // Always [], because the fetch request hasn't completed yet.
  })

Therefore, as a rule of thumb, whenever your operation encounters a promise, return it and defer its handling to the next then handler.

const listOfIngredients = []
 
doSomething()
  .then((url) =>
    fetch(url)
      .then((res) => res.json())
      .then((data) => {
        listOfIngredients.push(data)
      }),
  )
  .then(() => {
    console.log(listOfIngredients)
  })
 
// OR
 
doSomething()
  .then((url) => fetch(url))
  .then((res) => res.json())
  .then((data) => {
    listOfIngredients.push(data)
  })
  .then(() => {
    console.log(listOfIngredients)
  })

Nesting

In the two examples above, the first one has one promise chain nested in the return value of another then() handler, while the second one uses an entirely flat chain. Simple promise chains are best kept flat without nesting, as nesting can be a result of careless composition. common mistakes.

Nesting is a control structure to limit the scope of catch statements. Specifically, a nested catch only catches failures in its scope and below, not errors higher up in the chain outside the nested scope. When used correctly, this gives greater precision in error recovery:

doSomethingCritical()
  .then((result) =>
    doSomethingOptional(result)
      .then((optionalResult) => doSomethingExtraNice(optionalResult))
      .catch((e) => {}),
  ) // Ignore if optional stuff fails; proceed.
  .then(() => moreCriticalStuff())
  .catch((e) => console.error(`Critical failure: ${e.message}`))

The inner error-silencing catch handler only catches failures from doSomethingOptional() and doSomethingExtraNice(), after which the code resumes with moreCriticalStuff(). Importantly, if doSomethingCritical() fails, its error is caught by the final (outer) catch only, and does not get swallowed by the inner catch handler.

Chaining after a catch

It's possible to chain after a failure, i.e. a catch, which is useful to accomplish new actions even after an action failed in the chain.

new Promise((resolve, reject) => {
  console.log("Initial")
  resolve()
})
  .then(() => {
    throw new Error("Something failed")
    console.log("Do this")
  })
  .catch(() => {
    console.error("Do that")
  })
  .then(() => {
    console.log("Do this, no matter what happened before")
  })
/* This will output the following text
Initial
Do that
Do this, no matter what happened before
*/

The text "Do this" is not displayed because the "Something failed" error caused a rejection.

Common Mistakes

There are some common mistakes to watch out for when composing promise chains. Several of these mistakes manifest in the following example:

Return in a Chain, Unnecessary Nesting & Forgetting a catch โŒ

// Bad example! Spot 3 mistakes!
doSomething()
  .then(function (result) {
    // Forgot to return promise from inner chain + unnecessary nesting
    doSomethingElse(result).then((newResult) => doThirdThing(newResult))
  })
  .then(() => doFourthThing())
// Forgot to terminate chain with a catch!

The first mistake is to not chain things together properly. This happens when we create a new promise but forget to return it. As a consequence, the chain is broken โ€” or rather, we have two independent chains racing. This means doFourthThing() won't wait for doSomethingElse() or doThirdThing() to finish, and will run concurrently with them โ€” which is likely unintended. Separate chains also have separate error handling, leading to uncaught errors.

The second mistake is to nest unnecessarily, enabling the first mistake. Nesting also limits the scope of inner error handlers, which if unintended can lead to uncaught errors.

The third mistake is forgetting to terminate chains with catch. Unterminated promise chains lead to uncaught promise rejections in most browsers

A good rule of thumb is to always either return or terminate promise chains, and as soon as you get a new promise, return it immediately, to flatten things:

doSomething()
  .then(function (result) {
    // If using a full function expression: return the promise
    return doSomethingElse(result)
  })
  // If using arrow functions: omit the braces and implicitly return the result
  .then((newResult) => doThirdThing(newResult))
  // Even if the previous chained promise returns a result, the next one
  // doesn't necessarily have to use it. You can pass a handler that doesn't
  // consume any result.
  .then((/* result ignored */) => doFourthThing())
  // Always end the promise chain with a catch handler to avoid any
  // unhandled rejections!
  .catch((error) => console.error(error))

## Now we have a single deterministic chain with proper error handling.

Error Handling

For three chains you might surely see three error handling in the pyramid of doom callback style of coding compared to only one in a promise

doSomething()
  .then((result) => doSomethingElse(result))
  .then((newResult) => doThirdThing(newResult))
  .then((finalResult) => console.log(`Got the final result: ${finalResult}`))
  .catch(failureCallback)

If there's an exception, the browser will look down the chain for .catch() handlers or onRejected.

This is very much modeled after how synchronous code works:

try {
  const result = syncDoSomething()
  const newResult = syncDoSomethingElse(result)
  const finalResult = syncDoThirdThing(newResult)
  console.log(`Got the final result: ${finalResult}`)
} catch (error) {
  failureCallback(error)
}

Promises solve a fundamental flaw with the callback pyramid of doom, by catching all errors, even thrown exceptions and programming errors. This is essential for functional composition of asynchronous operations.

Composition / Promise Concurrency

The Promise class offers four static methods to facilitate async task concurrency: There are four composition tools for running asynchronous operations concurrently: Promise.all()Promise.allSettled()Promise.any(), and Promise.race().

  • ๐Ÿ’ก Promise.all()
    • โžก๏ธ Fulfills when all of the promises fulfill; rejects when any of the promises rejects.
  • ๐Ÿ’ก Promise.allSettled()
    • โžก๏ธ Fulfills when all promises settle.
  • ๐Ÿ’ก Promise.any()
    • โžก๏ธ Fulfills when any of the promises fulfills; rejects when all of the promises reject.
  • ๐Ÿ’ก Promise.race() - โžก๏ธ Settles when any of the promises settles. In other words, fulfills when any of the promises fulfills; rejects when any of the promises rejects.

    All these methods take an iterable of promises (thenables, to be exact) and return a new promise. They all support subclassing, which means they can be called on subclasses of Promise, and the result will be a promise of the subclass type.

Promise.all()

The Promise.all() static method takes an iterable of promises as input and returns a single Promise. This returned promise fulfills when all of the inputโ€™s promises fulfill (including when an empty iterable is passed), with an array of the fulfillment values. It rejects when any of the inputโ€™s promises rejects, with this first rejection reason.

const promise1 = Promise.resolve(3)
const promise2 = 42
const promise3 = new Promise((resolve, reject) => {
  setTimeout(resolve, 1000, "foo")
})
 
Promise.all([promise1, promise2, promise3]).then((values) => {
  console.log(values)
})
// Expected output: Array [3, 42, "foo"]

Syntax

Promise.all(iterable)

Description

The Promise.all() method is one of the promise concurrency methods. It can be useful for aggregating the results of multiple promises.

It is typically used when there are multiple related asynchronous tasks that the overall code relies on to work successfully all of whom we want to fulfill before the code execution continues.

Info Promise.all() will reject immediately upon any of the input promises rejecting. In comparison, the promise returned by Promise.allSettled() will wait for all input promises to complete, regardless of whether or not one rejects. Use allSettled() if you need the final result of every promise in the input iterable.

Using promise.all()

Promise.all waits for all fulfillments (or the first rejection).

// All values are non-promises, so the returned promise gets fulfilled
const p = Promise.all([1, 2, 3])
// The only input promise is already fulfilled,
// so the returned promise gets fulfilled
const p2 = Promise.all([1, 2, 3, Promise.resolve(444)])
// One (and the only) input promise is rejected,
// so the returned promise gets rejected
const p3 = Promise.all([1, 2, 3, Promise.reject(555)])
 
// Using setTimeout, we can execute code after the queue is empty
setTimeout(() => {
  console.log(p)
  console.log(p2)
  console.log(p3)
})
 
// Logs:
// Promise { <state>: "fulfilled", <value>: Array[3] }
// Promise { <state>: "fulfilled", <value>: Array[4] }
// Promise { <state>: "rejected", <reason>: 555 }

Info Promise.all resolves synchronously if and only if the iterable passed is empty:

const p = Promise.all([]) // Will be immediately resolved
const p2 = Promise.all([1337, "hi"]) // Non-promise values are ignored, but the evaluation is done asynchronously
console.log(p)
console.log(p2)
setTimeout(() => {
  console.log("the queue is now empty")
  console.log(p2)
})
 
// Logs:
// Promise { <state>: "fulfilled", <value>: Array[0] }
// Promise { <state>: "pending" }
// the queue is now empty
// Promise { <state>: "fulfilled", <value>: Array[2] }

Promise.allSettled()

The Promise.allSettled() static method takes an iterable of promises as input and returns a single Promise. This returned promise fulfills when all of the inputโ€™s promises settle (including when an empty iterable is passed), with an array of objects that describe the outcome of each promise.

const promise1 = Promise.resolve(3)
const promise2 = new Promise((resolve, reject) => setTimeout(reject, 100, "foo"))
const promises = [promise1, promise2]
 
Promise.allSettled(promises).then((results) =>
  results.forEach((result) => console.log(result.status)),
)
 
// Expected output:
// "fulfilled"
// "rejected"

Syntax

Promise.allSettled(iterable)

Return value

A Promise that is:

  • Already fulfilled, if the iterable passed is empty.

  • Asynchronously fulfilled, when all promises in the given iterable have settled (either fulfilled or rejected). The fulfillment value is an array of objects, each describing the outcome of one promise in the iterable, in the order of the promises passed, regardless of completion order. Each outcome object has the following properties:

  • โœ… status

    • A string, either โ€œfulfilledโ€ or โ€œrejectedโ€, indicating the eventual state of the promise.
  • โœ… value

    • Only present if status is โ€œfulfilledโ€. The value that the promise was fulfilled with.
  • โœ… reason

    • Only present if status is โ€œrejectedโ€. The reason that the promise was rejected with.

If the iterable passed is non-empty but contains no pending promises, the returned promise is still asynchronously (instead of synchronously) fulfilled.

Example

Promise.allSettled([
  Promise.resolve(33),
  new Promise((resolve) => setTimeout(() => resolve(66), 0)),
  99,
  Promise.reject(new Error("an error")),
]).then((values) => console.log(values))
 
// [
//   { status: 'fulfilled', value: 33 },
//   { status: 'fulfilled', value: 66 },
//   { status: 'fulfilled', value: 99 },
//   { status: 'rejected', reason: Error: an error }
// ]

Promise.race()

Gotchas

  1. Fastest Wins:

    • Gotcha: Promise.race resolves or rejects as soon as the first Promise in the input array settles (either fulfills or rejects). This means that it doesnโ€™t wait for all Promises to complete, only the first one.
    • Understanding: Use Promise.race when you want to act on the result of the first settled Promise, such as implementing a timeout mechanism.
  2. No Cancellation:

    • Gotcha: Unlike some other asynchronous patterns, Promise.race does not inherently support cancellation of the remaining Promises once one has settled.
    • Understanding: If you need to cancel other Promises in the race after one settles, youโ€™ll need to implement custom cancellation logic, which can be complex.
  3. Race Conditions:

    • Gotcha: When using Promise.race, be aware of potential race conditions. If multiple Promises resolve or reject at nearly the same time, it can lead to unpredictable behavior.
    • Understanding: Ensure that the Promises youโ€™re racing against are designed to handle such scenarios gracefully.
  4. Timeouts:

    • Gotcha: Promise.race is commonly used for implementing timeouts. However, if the timeout Promise resolves first, it wonโ€™t automatically cancel the other Promises; they will continue running.
    • Understanding: Implement your own logic to handle the cancellation of other Promises when a timeout occurs if necessary.
  5. Single Winner:

    • Gotcha: Promise.race returns the result (fulfillment value or rejection reason) of the first settled Promise. If you need to capture and handle multiple outcomes, consider alternatives like Promise.all.
  6. Error Handling:

    • Gotcha: Ensure that you handle errors effectively within each Promise in the race. A rejection of any Promise will cause Promise.race to reject immediately, so you should handle errors within each Promise to prevent unhandled promise rejections.
    • Understanding: Include error handling (.catch()) within each Promise or catch errors after the Promise.race operation to avoid unhandled rejections.

Promise.any()

  1. Fulfillment Only:

    • Gotcha: Promise.any resolves as soon as the first Promise in the input array fulfills. Unlike Promise.race, it does not consider rejections as a signal to resolve.
    • Understanding: Use Promise.any when you specifically want to capture the fulfillment value of the first resolved Promise, and youโ€™re not concerned about rejections.
  2. Handling Rejections:

    • Gotcha: If all Promises in the input array reject, Promise.any will reject with an AggregateError containing all the rejection reasons. It wonโ€™t resolve with a fulfillment value.
    • Understanding: Be prepared to handle the AggregateError that may contain multiple rejection reasons. You can use methods like .catch() or Promise.allSettled to handle rejections gracefully.
  3. No Cancellation:

    • Gotcha: Like Promise.race, Promise.any does not inherently support cancellation of the remaining Promises once one has fulfilled.
    • Understanding: If you need to cancel other Promises in the race after one fulfills, youโ€™ll need to implement custom cancellation logic.
  4. Multiple Winners:

    • Gotcha: If multiple Promises in the input array fulfill simultaneously (e.g., with the same microtask), the behavior may be unpredictable, as Promise.any resolves with the first fulfilled Promise.
    • Understanding: Ensure that the Promises youโ€™re using with Promise.any do not produce simultaneous fulfillments to maintain predictable behavior.
  5. Error Handling:

    • Gotcha: While Promise.any is designed to capture the fulfillment value of the first resolved Promise, itโ€™s still important to handle errors within each Promise in the input array to prevent unhandled promise rejections.
    • Understanding: Include error handling (.catch()) within each Promise or catch errors after the Promise.any operation to avoid unhandled rejections.

Fetch

The Fetch API provides a JavaScript interface for accessing and manipulating parts of the protocol, such as requests and responses. It also provides a global fetch() method that provides an easy, logical way to fetch resources asynchronously across the network.

Unlike XMLHttpRequest that is a callback-based API, Fetch is promise-based and provides a better alternative that can be easily used in service workers. Fetch also integrates advanced HTTP concepts such as CORS and other extensions to HTTP.

fetch() allows you to make network requests similar to XMLHttpRequest (XHR). The main difference is that the Fetch API uses Promises, which enables a simpler and cleaner API, avoiding callback hell and having to remember the complex API of XMLHttpRequest.

XMLHttpRequest vs Fetch

Letโ€™s start by comparing a simple example implemented with an XMLHttpRequest and then with fetch. We just want to request a URL, get a response and parse it as JSON

XMLHttpRequest

An XMLHttpRequest would need two listeners to be set to handle the success and error cases and a call to open() and send()

function reqListener() {
  var data = JSON.parse(this.responseText)
  console.log(data)
}
 
function reqError(err) {
  console.log("Fetch Error :-S", err)
}
 
var oReq = new XMLHttpRequest()
oReq.onload = reqListener
oReq.onerror = reqError
oReq.open("get", "./api/some.json", true)
oReq.send()

Fetch

Our fetch request looks a little like this:

fetch("https://reqres.in/api/users")
  .then(function (response) {
    if (response.status !== 200) {
      console.log("Looks like there was a problem. Status Code: " + response.status)
      return
    } // Examine the text in the response
    response.json().then(function (data) {
      console.log(data)
    })
  })
  .catch(function (err) {
    console.log("Fetch Error :-S", err)
  })

We start by checking that the response status is 200 before parsing the response as JSON.

The response of a fetch() request is a Stream object, which means that when we call the json() method, a Promise is returned since the reading of the stream will happen asynchronously.

Fetch

fetch(resource)
fetch(resource, options)

Paramerters

resource

  • โžก๏ธ This defines the resource that you wish to fetch. This can either be:
    • ๐Ÿ”– A string or any other object with a stringifier โ€” including a URL object โ€” that provides the URL of the resource you want to fetch.
    • ๐Ÿ”– A Request object.

Options

An object containing any custom settings that you want to apply to the request. The possible options are:

method

The request method, e.g., "GET""POST". The default is "GET".

headers
body
mode
credentials
omit
same-origin
include
cahce

โ€ฆ

Overview

  • ๐Ÿ’ก The global fetch() method starts the process of fetching a resource from the network, returning a promise which is fulfilled once the response is available.
  • ๐Ÿ’ก The promise resolves to the Response object representing the response to your request.
  • ๐Ÿ”ฅ No rejection on a 404.

    A fetch() promise only rejects when a network error is encountered (which is usually when there's a permissions issue or similar). A fetch() promise does not reject on HTTP errors (404, etc.). Instead, a then() handler must check the Response.ok and/or Response.status properties.

This will return a promise that contains the response data. This response data contains properties for the status as well as methods for converting the raw response data to JSON, text, or other formats.

fetch("https://jsonplaceholder.typicode.com/users").then((res) => {
  console.log(res.ok) // true
  console.log(res.status) // 200
  return res.json()
})

The highlighted code above is calling the json method on our response and it is returning that from the .then function. This is because the json method also returns a promise that evaluates to the JSON data from our response. We can chain a second .then to get the data from the json method.

fetch("https://jsonplaceholder.typicode.com/users")
  .then((res) => res.json())
  .then((data) => console.log(data))
// [{ userOne }, { userTwo }, ...]

This is what most of your fetch requests will look like if you are fetching data from a JSON api. We first fetch the URL, then we convert the response to JSON, and finally we use the data in the final .then.

Response Metadata

In the previous example we looked at the status of the Response object as well as how to parse the response as JSON. Other metadata we may want to access, like headers, are illustrated below.

fetch("./api/some.json")
  .then(function (response) {
    if (response.status !== 200) {
      console.log("There was a problem, Status Code: " + response.status)
      return
    }
 
    // Examine the text in the response
    response.json().then(function (data) {
      console.log(data)
    })
  })
  .catch(function (err) {
    console.log("Fetch Error :-S", err)
  })

Response Metadata

fetch("https://jsonplaceholder.typicode.com/users").then(function (response) {
  console.log(response.headers.get("Content-Type")) //application/json; charset=utf-8
  console.log(response.headers.get("Date")) //Thu, 07 Sep 2023 12:40:21 GMT
  console.log(response.status) //200
  console.log(response.statusText) //OK
  console.log(response.type) //basic
  console.log(response.url) //https://jsonplaceholder.typicode.com/users
})

Response Types

When we make a fetch request, the response will be given a response.type of โ€basicโ€, โ€corsโ€ or โ€opaqueโ€. These types indicate where the resource has come from and can be used to inform how you should treat the response object.

  • Basic
    • When a request is made for a resource on the same origin, the response will have a basic type and there arenโ€™t any restrictions on what you can view from the response.
  • Cors
    • If a request is made for a resource on another origin which returns the CORs headers, then the type is cors.
  • opaque
    • response is for a request made for a resource on a different origin that doesnโ€™t return CORS headers. With an opaque response we wonโ€™t be able to read the data returned or view the status of the request, meaning we canโ€™t check if the request was successful or not.

You can define a mode for a fetch request such that only certain requests will resolve. The modes you can set are as follows:

  • ๐Ÿ”– same-origin
    • โžก๏ธ only succeeds for requests for assets on the same origin, all other requests will reject.
  • ๐Ÿ”– cors
    • โžก๏ธ will allow requests for assets on the same-origin and other origins which return the appropriate CORs headers.
  • ๐Ÿ”– cors-with-forced-preflight
    • โžก๏ธ will always perform a preflight check before making the actual request.
  • ๐Ÿ”– no-cors
    • โžก๏ธ is intended to make requests to other origins that do not have CORS headers and result in an opaque response, but as stated, this isnโ€™t possible in the window global scope at the moment.

To define the mode, add an options object as the second parameter in the fetch request and define the mode in that object:

fetch("http://some-site.com/cors-enabled/some.json", { mode: "cors" })
  .then(function (response) {
    return response.text()
  })
  .then(function (text) {
    console.log("Request successful", text)
  })
  .catch(function (error) {
    log("Request failed", error)
  })

Chaining Promises

One of the great features of promises is the ability to chain them together. For fetch, this allows you to share logic across fetch requests.

If you are working with a JSON API, youโ€™ll need to check the status and parse the JSON for each response. You can simplify your code by defining the status and JSON parsing in separate functions which return promises, freeing you to only worry about handling the final data and the error case.

function status(response) {
  if (response.status >= 200 && response.status < 300) {
    return Promise.resolve(response)
  } else {
    return Promise.reject(new Error(response.statusText))
  }
}
 
function json(response) {
  return response.json()
}
 
fetch("users.json")
  .then(status)
  .then(json)
  .then(function (data) {
    console.log("Request succeeded with JSON response", data)
  })
  .catch(function (error) {
    console.log("Request failed", error)
  })

We define the status function which checks the response.status and returns the result of Promise.resolve() or Promise.reject(), which return a resolved or rejected Promise. This is the first method called in our fetch() chain, if it resolves, we then call our json() method which again returns a Promise from the response.json() call. After this we have an object of the parsed JSON. If the parsing fails the Promise is rejected and the catch statement executes.

The great thing with this is that you can share the logic across all of your fetch requests, making code easier to maintain, read and test.

POST Request

Itโ€™s not uncommon for web apps to want to call an API with a POST method and supply some parameters in the body of the request. To do this we can set the method and body parameters in the fetch() options.

fetch(url, {
  method: "post",
  headers: {
    "Content-type": "application/x-www-form-urlencoded; charset=UTF-8",
  },
  body: "foo=bar&lorem=ipsum",
})
  .then(json)
  .then(function (data) {
    console.log("Request succeeded with JSON response", data)
  })
  .catch(function (error) {
    console.log("Request failed", error)
  })

Sending Credentials with a Fetch Request

Should you want to make a fetch request with credentials such as cookies, you should set the credentials of the request to โ€œincludeโ€.

fetch(url, {
  credentials: "include",
})

Async/Await

Info async/await is a modern JavaScript feature that simplifies asynchronous code and makes it more readable and maintainable. It is built on top of Promises and provides a way to write asynchronous code in a more synchronous style.

With async/await, you can write non-blocking code that appears sequential, making it easier to understand and debug asynchronous operations like network requests, file I/O, and timers.

Syntax

Info Note: There cannot be a line terminator between async and function, otherwise a semicolon is automatically inserted, causing async to become an identifier and the rest to become a function declaration.

async function name(param0) {
  statements
}
async function name(param0, param1) {
  statements
}
async function name(param0, param1, /* โ€ฆ, */ paramN) {
  statements
}

Parameters

  • ๐Ÿ”ฅ name
    • โžก๏ธ The functionโ€™s name.
  • ๐Ÿ”ฅ param (Optional)
    • โžก๏ธ The name of a formal parameter for the function. For the parametersโ€™ syntax, see the Functions reference.
  • ๐Ÿ”ฅ statements (Optional)
    • โžก๏ธ The statements comprising the body of the function. The await mechanism may be used.

The async keyword is used to define an asynchronous function, which returns a Promise. Inside an async function, you can use the await keyword to pause the functionโ€™s execution until a Promise is resolved. The await keyword can only be used inside an async function.

async function fetchData() {
  try {
    const response = await fetch("https://api.example.com/data")
    const data = await response.json()
    return data
  } catch (error) {
    console.error("Error:", error)
    throw error
  }
}
  • โžก๏ธ fetchData is an async function that fetches data from an API.
  • โžก๏ธ await fetch('https://api.example.com/data') pauses the function until the HTTP request is complete.
  • โžก๏ธ await response.json() pauses the function until the JSON parsing is complete.
  • โžก๏ธ Errors are caught using a try/catch block.

Async functions always return a promise. If the return value of an async function is not explicitly a promise, it will be implicitly wrapped in a promise.

async function foo() {
  return 1
}

It is similar to:

function foo() {
  return Promise.resolve(1)
}

The async function declaration creates a binding of a new async function to a given name. The await keyword is permitted within the function body, enabling asynchronous, promise-based behavior to be written in a cleaner style and avoiding the need to explicitly configure promise chains.

function resolveAfter2Seconds() {
  return new Promise((resolve) => {
    setTimeout(() => {
      resolve("resolved")
    }, 2000)
  })
}
 
async function asyncCall() {
  console.log("calling")
  const result = await resolveAfter2Seconds()
  console.log(result)
  // Expected output: "resolved"
}
 
asyncCall()

Usage

To call an async function and handle its result, you can use .then() and .catch() or try/catch:

Yes this is great compared to the callbacks but check bellow for the async way!

fetchData()
  .then((data) => {
    // Handle data
  })
  .catch((error) => {
    // Handle error
  })

With Async Await you get an even clear syntax!

try {
  const data = await fetchData()
  // Handle data
} catch (error) {
  // Handle error
}

Parallel Execution

You can use Promise.all to run multiple asynchronous operations in parallel:

async function fetchMultiple() {
  const [data1, data2] = await Promise.all([fetchData1(), fetchData2()])
  // Handle data1 and data2
}

Sequential Execution

To execute asynchronous operations sequentially, use await in a loop:

async function fetchSequentially(urls) {
  const results = []
  for (const url of urls) {
    const data = await fetchData(url)
    results.push(data)
  }
  return results
}

AbortController: abort() Method

The abort() method of the AbortController interface aborts a DOM request before it has completed.

This is able to abort fetch requests, the consumption of any response bodies, or streams.

abort()
abort(reason)

Parameters

  • โžก๏ธ reason Optional
    • [/] The reason why the operation was aborted, which can be any JavaScript value. If not specified, the reason is set to โ€œAbortErrorโ€ DOMException.

Return Value None (undefined)

Example

In the following snippet, we aim to download a video using the Fetch API.

We first create a controller using the AbortController() constructor, then grab a reference to its associated AbortSignal object using the AbortController.signal property.

When the fetch request is initiated, we pass in the AbortSignal as an option inside the requestโ€™s options object (the {signal} below). This associates the signal and controller with the fetch request and allows us to abort it by calling AbortController.abort(), as seen below in the second event listener.

const controller = new AbortController()
const signal = controller.signal
 
const url = "video.mp4"
const downloadBtn = document.querySelector(".download")
const abortBtn = document.querySelector(".abort")
 
downloadBtn.addEventListener("click", fetchVideo)
 
abortBtn.addEventListener("click", () => {
  controller.abort()
  console.log("Download aborted")
})
 
function fetchVideo() {
  fetch(url, { signal })
    .then((response) => {
      console.log("Download complete", response)
    })
    .catch((err) => {
      console.error(`Download error: ${err.message}`)
    })
}

Note: When abort() is called, the fetch() promise rejects with an Error of type DOMException, with name AbortError.

Step By Step GUIDE

Info Overview To use the AbortController, create an instance of it, and pass its signal into a promise.

// Creation of an AbortController signal
const controller = new AbortController()
const signal = controller.signal
 
// Call a promise, with the signal injected into it
doSomethingAsync({ signal })
  .then((result) => {
    console.log(result)
  })
  .catch((err) => {
    if (err.name === "AbortError") {
      console.log("Promise Aborted")
    } else {
      console.log("Promise Rejected")
    }
  })

Inside the actual promise you listen for the abort event to be fired on the given signal, upon which the promise will be cancelled.

// Unsupported browser
if (!("AbortController" in window)) {
  document.getElementById("log").textContent =
    "Your browser does not support AbortController. Please use Firefox or Edge for the time being."
}
 
// Browser with support
else {
  // Example Promise, which takes signal into account
  function doSomethingAsync({ signal }) {
    if (signal.aborted) {
      return Promise.reject(new DOMException("Aborted", "AbortError"))
    }
 
    return new Promise((resolve, reject) => {
      document.getElementById("log").textContent = "Promise Started"
 
      // Something fake async
      const timeout = setTimeout(resolve, 2500, "Promise Resolved")
 
      // Listen for abort event on signal
      signal.addEventListener("abort", () => {
        clearTimeout(timeout)
        reject(new DOMException("Aborted", "AbortError"))
      })
    })
  }
 
  // Creation of an AbortController signal
  const controller = new AbortController()
  const signal = controller.signal
 
  // Start our promise, catching errors (including abort)
  const start = (e) => {
    doSomethingAsync({ signal })
      .then((result) => {
        document.getElementById("log").textContent = result
      })
      .catch((err) => {
        if (err.name === "AbortError") {
          document.getElementById("log").textContent = "Promise Aborted"
        } else {
          document.getElementById("log").textContent = "Promise Rejected"
        }
      })
  }
 
  // Stop the promise (by calling abort)
  const stop = (e) => {
    controller.abort()
  }
 
  // Hook events to buttons
  document.getElementById("start").addEventListener("click", start)
  document.getElementById("stop").addEventListener("click", stop)
}

Tips and tricks

Temporal Dependency

Temporal dependency, also known as time-based dependency or time dependency, refers to the relationship between different events or processes in a system based on the sequence or timing of their occurrence. In other words, it describes how one event or process relies on the timing or order of another event or process.

Consider Promise.allSettled() Instead Promise.all()

These two methods behave differently, and itโ€™s completely fine to use one or the other. But, before Promise.allSettled() was introduced in ES2020, developers were desperately trying to โ€˜fixโ€™ Promise.all() behavior. Namely, Promise.all() would reject upon any of the promises rejecting, with the return value of the first rejected promise. What if we still want to get the values of the resolved promises?

A workaround solution is to add a custom catch() method to every promise in the input promise array. In the example below, the catch() method will set the value of the associated promise to โ€˜rejectedโ€™ in case the promise gets rejected.

const promises = fileNames.map((file) => fetchFile(file).catch(() => "rejected"))
const results = await Promise.all(promises)
const resolved = results.filter((result) => result !== "rejected")

With Promise.allSettled() we get an array of objects with the outcome of each promise out of the box. It will never reject - instead, it will wait for all promises passed in the array to either resolve or reject.

const promises = fileNames.map(fetchFile)
const results = await Promise.allSettled(promises)
const resolved = results
  .filter((result) => result.status === "fulfilled")
  .map((result) => result.value)

Key takeaway tips: UsePromise.all() to request in parallel and fail fast upon rejection, without waiting for all promises to resolve. Use Promise.allSettled() to request in parallel, never fail and get all the results.

Advanced Error Handling Async/Await & Promises

Advanced Error Handling: (NODE Environment)

You can use a global unhandledRejection event to handle unhandled promise rejections at the application level.

process.on("unhandledRejection", (reason, promise) => {
  console.error("Unhandled Rejection at:", promise, "reason:", reason)
  // Handle or log the error here
})

Global window Object:

In a browser environment, you can use the global window object to handle unhandled promise rejections. You can attach an event listener to the unhandledrejection event like this:

window.addEventListener("unhandledrejection", (event) => {
  console.error("Unhandled Promise Rejection:", event.reason)
  // Handle or log the error here
})

This approach allows you to catch unhandled promise rejections at the browser level and provide custom error handling.

window.onerror:

You can also use the window.onerror event handler to catch unhandled promise rejections:

window.onerror = function (message, source, lineno, colno, error) {
  console.error("Unhandled Error:", error)
  // Handle or log the error here
}

This event handler captures various types of errors, including unhandled promise rejections.