Javascript async: Difference between revisions

From bibbleWiki
Jump to navigation Jump to search
 
(36 intermediate revisions by the same user not shown)
Line 1: Line 1:
=Introduction=
=Typical Problem=
==Example Code==
==Failing Code==
===Typical Failing Code===
<syntaxhighlight lang="javascript">
<syntaxhighlight lang="javascript">
export function raceCondition() {
export function raceCondition() {
Line 32: Line 31:
</syntaxhighlight>
</syntaxhighlight>
This may fail because it finishes the second request before the first. I.E. we did not wait for the first request before using the second request.
This may fail because it finishes the second request before the first. I.E. we did not wait for the first request before using the second request.
===Callback Pyramid Of Doom===
 
==Callback Pyramid Of Doom==
Moving Second Request to after First Request solves the problem but this know as '''Callback Pyramid Of Doom''' as each request will indent on the previous request
Moving Second Request to after First Request solves the problem but this know as '''Callback Pyramid Of Doom''' as each request will indent on the previous request
<syntaxhighlight lang="javascript">
<syntaxhighlight lang="javascript">
Line 63: Line 63:
}
}
</syntaxhighlight>
</syntaxhighlight>
===Promise===
 
=Promises=
==Introduction==
A Promise is "Object that represents the eventual completion (or failure) of an asyncronous operation, and its resulting value"
A Promise is "Object that represents the eventual completion (or failure) of an asyncronous operation, and its resulting value"
A Promise can have three states
A Promise can have three states
Line 69: Line 71:
* Fulfilled
* Fulfilled
* Rejected
* Rejected
(Settled/Resolved means either Fulfilled or Rejected)
(Settled/Resolved means either Fulfilled or Rejected)<br>
'''Note''' Promises are not lazy, i.e. not like yield.
'''Note''' Promises are not lazy, i.e. not like yield in c# they execute immediately.
==Standard try catch Promise==
This is how to catch a standard promise
<syntaxhighlight lang="javascript">
export function getCatch() {
  axios
    .get("http://localhost:3000/orders/11")
    .then(({ data }) => {
      setText(JSON.stringify(data));
    })
    .catch((err) => {
      setText(err);
    })
    .finally(() => console.log("Done"));
}
</syntaxhighlight>
 
==Chaining Promises==
Below is an example of chaining Promises. You can catch errors within the code but you would have to make sure you are throwing the appropriate arguments for the next then.
<syntaxhighlight lang="javascript">
export function chainCatch() {
  axios
    .get("http://localhost:3000/orders/1")
    .then(({ data }) => {
      return axios.get(
        `http://localhost:3000/addresses/${data.shippingAddress}`
      );
    })
    .then(({ data }) => {
      setText(data.my.city);
    })
    .catch((err) => {
      setText(err);
    })
    .finally(() => console.log("Done"));
}
</syntaxhighlight>
=Creating Promises=
==Promise Arguments==
The promise takes something called an executor function
<syntaxhighlight lang="javascript">
let promise = new Promise((resolve, reject) =>
  {
...
  })
</syntaxhighlight>
 
==Promise with a request==
So we can now create a request assigning the resolve and the reject functions to the functions in XMLHttpRequest.
<syntaxhighlight lang="javascript">
  let request = new Promise((resolve, reject) => {
    // Create a request
    let xhr = new XMLHttpRequest();
 
    // Execute the request
    xhr.open("GET", "http://localhost:3000/users/1");
 
    // Assign onload to the resolve function
    xhr.onload = () => resolve(xhr.responseText);
 
    // Assign onerror to the reject function
    xhr.onerror = () => reject(xhr.statusText);
 
    // Do the send
    xhr.send();
  });
 
  // On success or failure of the request set the text
  request.then((result) => setText(result)).catch((reason) => setText(reason));
</syntaxhighlight>
 
==Managing a failed 404 request==
For a user where it exists the above code works perfectly however when a user does not exist the request is successful but not from an application point of view. Let's improve it.
<syntaxhighlight lang="javascript">
  let request = new Promise((resolve, reject) => {
    // Create a request
    let xhr = new XMLHttpRequest();
 
    // Execute the request
    xhr.open("GET", "http://localhost:3000/users/No_Valid");
 
    // Assign onload to the resolve function
    xhr.onload = () => {
      if (xhr.status === 200) {
        resolve(xhr.responseText);
      } else {
        reject(xhr.statusText);
      }
    };
 
    // Assign onerror to the reject function
    xhr.onerror = () => reject(xhr.statusText);
 
    // Do the send
    xhr.send();
  });
 
  // On success or failure of the request set the text
  request.then((result) => setText(result)).catch((reason) => setText(reason));
 
</syntaxhighlight>
 
==Promise All==
This is used to send multiple requests. The Promise.All function will waiting until either '''all''' fulfill or '''one''' rejects. Here is an example using axios.
<syntaxhighlight lang="javascript">
  let itemCategories = axios.get("http://localhost:3000/itemCategories");
  let orderStatuses = axios.get("http://localhost:3000/orderStatuses");
  let userTypes = axios.get("http://localhost:3000/userTypes");
 
  Promise.all([itemCategories, orderStatuses, userTypes])
    .then(([categories, statuses, types]) => {
      setText("");
      setText(JSON.stringify(categories.data));
      setText(JSON.stringify(statuses.data));
      setText(JSON.stringify(types.data));
    })
    .catch((reasons) => {
      setText(reasons);
    });
</syntaxhighlight>
==Promise allSettled==
The previous Promise All will exit on error which may be not what you want. Promise Settled will process until a result is available for every Promise. In general the catch is therefore not required but people code it regardless.
<syntaxhighlight lang="javascript">
  let itemCategories = axios.get("http://localhost:3000/itemCategories");
  let orderStatuses = axios.get("http://localhost:3000/orderStatuses");
  let userTypes = axios.get("http://localhost:3000/userTypesX");
 
  Promise.allSettled([itemCategories, orderStatuses, userTypes])
    .then((values) => {
      console.log("values", values);
      let results = values.map((v) => {
        if (v.status === "fulfilled") {
          return `FULFILLED: ${JSON.stringify(v.value.data[0])} |`;
        }
        return `REJECTED: ${v.reason.message} |`;
      });
      setText(results);
    })
    .catch((reasons) => {
      setText(reasons);
    });
</syntaxhighlight>
The shape of the data is slightly different as a consequence. An array is returned with either 'fulfilled' in the status or 'rejected'. The rejected contain the error the fulfilled contain the values.<br><br>
[[File:Promise settled.png|400px]]
 
For compatibility go to https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/allSettled
==Promise race==
Promise race will run all promises and stop when the first one is settled regardless of the outcome. This could be used when trying to get data from several endpoints.
<syntaxhighlight lang="javascript">
  let server1 = axios.get("http://localhost:3000/users");
  let server2 = axios.get("http://localhost:3001/users");
 
  Promise.race([server1, server2])
    .then((users) => {
      setText(JSON.stringify(users.data));
    })
    .catch((reason) => {
      setText(reason);
    });
</syntaxhighlight>
=Aync/Await=
==Async==
Async functions can be declare either using standard functions or arrow functions.
<syntaxhighlight lang="javascript">
async function getNames() {
    return [];
}
 
const getNames = async () => {
    return [];
}
</syntaxhighlight>
 
==Await==
Await can only be used with an async function. In the example below doSomething awaits someFunc to finish. However getAddresses() does '''not''' await for getNames() to finish.
<syntaxhighlight lang="javascript">
const getNames = async () => {
    await someFunc();
    doSomething();
}
 
getNames();
getAddresses();
</syntaxhighlight>
 
==Concurrent Calls==
For concurrent calls you can await on the destructure of the data coming back.
<syntaxhighlight lang="javascript">
  const orderStatus = axios.get("http://localhost:3000/orderStatuses");
  const orders = axios.get("http://localhost:3000/orders");
 
  setText("");
 
  const { data: statuses } = await orderStatus;
  const { data: order } = await orders;
 
  appendText(JSON.stringify(statuses));
  appendText(JSON.stringify(order[0]));
</syntaxhighlight>
 
==Parallel Calls==
Combining promises and async/await you can process requests in parallel
<syntaxhighlight lang="javascript">
  setText("");
 
  await Promise.all([
    (async () => {
      const { data } = await axios.get("http://localhost:3000/orderStatuses");
      appendText(JSON.stringify(data));
    })(),
    (async () => {
      const { data } = await axios.get("http://localhost:3000/orders");
      appendText(JSON.stringify(data));
    })(),
  ]);
</syntaxhighlight>
=Resources=
I used the git repository for this.
<syntaxhighlight lang="javascript">
git clone http://github.com/taylonr/async-programming-promises
</syntaxhighlight>

Latest revision as of 01:37, 14 August 2020

Typical Problem

Failing Code

export function raceCondition() {
  let xhr = new XMLHttpRequest();
  let statuses = [];
  xhr.open("GET", "http://localhost:3000/ordersStatuses");

  // Success
  xhr.onload = () => {
    statuses = JSON.parse(xhr.responseText);
  };

  let xhr2 = new XMLHttpRequest();
  xhr2.open("GET", "http://localhost:3000/orders/1");

  // Success
  xhr2.onload = () => {
    const order = JSON.parse(xhr2.responseText);
    const description = status.map((t) => {
      if (t.id === order.orderStatusId) {
        return t.description;
      }
    })[0];

    setText("Order Status: ${description}");
  };

  xhr2.send();
}

This may fail because it finishes the second request before the first. I.E. we did not wait for the first request before using the second request.

Callback Pyramid Of Doom

Moving Second Request to after First Request solves the problem but this know as Callback Pyramid Of Doom as each request will indent on the previous request

export function raceCondition() {
  let xhr = new XMLHttpRequest();
  let statuses = [];
  xhr.open("GET", "http://localhost:3000/ordersStatuses");

  // Success
  xhr.onload = () => {
    statuses = JSON.parse(xhr.responseText);

    let xhr2 = new XMLHttpRequest();
    xhr2.open("GET", "http://localhost:3000/orders/1");

    // Success
    xhr2.onload = () => {
      const order = JSON.parse(xhr2.responseText);
      const description = status.map((t) => {
        if (t.id === order.orderStatusId) {
          return t.description;
        }
      })[0];

      setText("Order Status: ${description}");
    };

    xhr2.send();
  };
}

Promises

Introduction

A Promise is "Object that represents the eventual completion (or failure) of an asyncronous operation, and its resulting value" A Promise can have three states

  • Pending
  • Fulfilled
  • Rejected

(Settled/Resolved means either Fulfilled or Rejected)
Note Promises are not lazy, i.e. not like yield in c# they execute immediately.

Standard try catch Promise

This is how to catch a standard promise

export function getCatch() {
  axios
    .get("http://localhost:3000/orders/11")
    .then(({ data }) => {
      setText(JSON.stringify(data));
    })
    .catch((err) => {
      setText(err);
    })
    .finally(() => console.log("Done"));
}

Chaining Promises

Below is an example of chaining Promises. You can catch errors within the code but you would have to make sure you are throwing the appropriate arguments for the next then.

export function chainCatch() {
  axios
    .get("http://localhost:3000/orders/1")
    .then(({ data }) => {
      return axios.get(
        `http://localhost:3000/addresses/${data.shippingAddress}`
      );
    })
    .then(({ data }) => {
      setText(data.my.city);
    })
    .catch((err) => {
      setText(err);
    })
    .finally(() => console.log("Done"));
}

Creating Promises

Promise Arguments

The promise takes something called an executor function

let promise = new Promise((resolve, reject) => 
  {
...
  })

Promise with a request

So we can now create a request assigning the resolve and the reject functions to the functions in XMLHttpRequest.

  let request = new Promise((resolve, reject) => {
    // Create a request
    let xhr = new XMLHttpRequest();

    // Execute the request
    xhr.open("GET", "http://localhost:3000/users/1");

    // Assign onload to the resolve function
    xhr.onload = () => resolve(xhr.responseText);

    // Assign onerror to the reject function
    xhr.onerror = () => reject(xhr.statusText);

    // Do the send
    xhr.send();
  });

  // On success or failure of the request set the text
  request.then((result) => setText(result)).catch((reason) => setText(reason));

Managing a failed 404 request

For a user where it exists the above code works perfectly however when a user does not exist the request is successful but not from an application point of view. Let's improve it.

  let request = new Promise((resolve, reject) => {
    // Create a request
    let xhr = new XMLHttpRequest();

    // Execute the request
    xhr.open("GET", "http://localhost:3000/users/No_Valid");

    // Assign onload to the resolve function
    xhr.onload = () => {
      if (xhr.status === 200) {
        resolve(xhr.responseText);
      } else {
        reject(xhr.statusText);
      }
    };

    // Assign onerror to the reject function
    xhr.onerror = () => reject(xhr.statusText);

    // Do the send
    xhr.send();
  });

  // On success or failure of the request set the text
  request.then((result) => setText(result)).catch((reason) => setText(reason));

Promise All

This is used to send multiple requests. The Promise.All function will waiting until either all fulfill or one rejects. Here is an example using axios.

  let itemCategories = axios.get("http://localhost:3000/itemCategories");
  let orderStatuses = axios.get("http://localhost:3000/orderStatuses");
  let userTypes = axios.get("http://localhost:3000/userTypes");

  Promise.all([itemCategories, orderStatuses, userTypes])
    .then(([categories, statuses, types]) => {
      setText("");
      setText(JSON.stringify(categories.data));
      setText(JSON.stringify(statuses.data));
      setText(JSON.stringify(types.data));
    })
    .catch((reasons) => {
      setText(reasons);
    });

Promise allSettled

The previous Promise All will exit on error which may be not what you want. Promise Settled will process until a result is available for every Promise. In general the catch is therefore not required but people code it regardless.

  let itemCategories = axios.get("http://localhost:3000/itemCategories");
  let orderStatuses = axios.get("http://localhost:3000/orderStatuses");
  let userTypes = axios.get("http://localhost:3000/userTypesX");

  Promise.allSettled([itemCategories, orderStatuses, userTypes])
    .then((values) => {
      console.log("values", values);
      let results = values.map((v) => {
        if (v.status === "fulfilled") {
          return `FULFILLED: ${JSON.stringify(v.value.data[0])} |`;
        }
        return `REJECTED: ${v.reason.message} |`;
      });
      setText(results);
    })
    .catch((reasons) => {
      setText(reasons);
    });

The shape of the data is slightly different as a consequence. An array is returned with either 'fulfilled' in the status or 'rejected'. The rejected contain the error the fulfilled contain the values.

For compatibility go to https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/allSettled

Promise race

Promise race will run all promises and stop when the first one is settled regardless of the outcome. This could be used when trying to get data from several endpoints.

  let server1 = axios.get("http://localhost:3000/users");
  let server2 = axios.get("http://localhost:3001/users");

  Promise.race([server1, server2])
    .then((users) => {
      setText(JSON.stringify(users.data));
    })
    .catch((reason) => {
      setText(reason);
    });

Aync/Await

Async

Async functions can be declare either using standard functions or arrow functions.

async function getNames() {
    return [];
}

const getNames = async () => {
    return [];
}

Await

Await can only be used with an async function. In the example below doSomething awaits someFunc to finish. However getAddresses() does not await for getNames() to finish.

const getNames = async () => {
    await someFunc();
    doSomething();
}

getNames();
getAddresses();

Concurrent Calls

For concurrent calls you can await on the destructure of the data coming back.

  const orderStatus = axios.get("http://localhost:3000/orderStatuses");
  const orders = axios.get("http://localhost:3000/orders");

  setText("");

  const { data: statuses } = await orderStatus;
  const { data: order } = await orders;

  appendText(JSON.stringify(statuses));
  appendText(JSON.stringify(order[0]));

Parallel Calls

Combining promises and async/await you can process requests in parallel

  setText("");

  await Promise.all([
    (async () => {
      const { data } = await axios.get("http://localhost:3000/orderStatuses");
      appendText(JSON.stringify(data));
    })(),
    (async () => {
      const { data } = await axios.get("http://localhost:3000/orders");
      appendText(JSON.stringify(data));
    })(),
  ]);

Resources

I used the git repository for this.

git clone http://github.com/taylonr/async-programming-promises