paint-brush
Velo Promises in Action: Key Tips to Call the Asynchronously-Run Functionsby@velo
1,571 reads
1,571 reads

Velo Promises in Action: Key Tips to Call the Asynchronously-Run Functions

by Velo by WixApril 1st, 2021
Read on Terminal Reader
Read this story w/o Javascript
tldt arrow

Too Long; Didn't Read

Velo is a full-stack development platform that empowers you to rapidly build, manage and deploy professional web apps. In Velo, you will often encounter asynchronous code when you call a function that cannot finish executing immediately and therefore returns a Promise. A Promise represents a value that is not necessarily known when the Promise is created because it depends on asynchronous communication. We discuss different ways to call such functions and wait for them to finish executing. There are two main ways to work with Promises that are returned from a function.

Company Mentioned

Mention Thumbnail

Coin Mentioned

Mention Thumbnail
featured image - Velo Promises in Action: Key Tips to Call the Asynchronously-Run Functions
Velo by Wix HackerNoon profile picture

Asynchronous code is code that doesn't necessarily run line-by-line in the order you've written it. In Velo, you will often encounter asynchronous code when you call a function that cannot finish executing immediately and therefore returns a Promise. Also, when you call a backend function that you've written from your page code, you must call it asynchronously.

For example, if you run a query on one of your database collections using the

find()
function, it takes some time for the query to be executed and for the results to be returned. So you have to wait until the operation is
finished before doing anything with the query results.

Consider the following code snippet where the

find()
function returns a Promise:

console.log("1")

wixData.query("myCollection")
  .find()
  .then( (results) => {
    console.log("2");
  } );

console.log("3");

The following will be logged to the console.

1
3
2

Notice that the 3 is logged before the

2
even though the
console.log()
calls do not appear in that order in the code. That is because the logging of
2
only occurs once the query is finished, but the rest of the code continues to run in the meantime.

In this article, we describe how to work with the functions in the Velo API that run asynchronously. We discuss different ways to call such functions and wait for them to finish executing. We do not present a general discussion of asynchronous programming in JavaScript. To learn more about using Promises in a general context, see Using Promises.

Promise

A Promise represents a value that is not necessarily known when the Promise is created because it depends on asynchronous communication. A
function that returns a promise is returning a contract to eventually produce a return value at some point in the future. A Promise is said to be fulfilled or resolved when its value is finally known.

There are two main ways to work with Promises that are returned from a function.

  • then()
    - Using this approach your code is generally a little more difficult to read, but you have more control over when your code runs. For example, if your code does not need to wait for a Promise to resolve, you can have other code run in the meantime, thereby improving overall performance.
  • async/await
    - Allows you to work with asynchronous code as if it was synchronous, making you code easier to read. However, you lose the option of running other code while waiting for a Promise to resolve.

Generally, using

async/await
is preferred when it does not affect the performance of your site because it makes for cleaner code. However, there are some cases where using then() improves performance and is worth the added complexity.

then( )

You can use the

then()
of a Promise to specify a function that will be run when the Promise resolves. The specified function receives the value that the Promise resolves to. So, any code that relies on the Promise's resolved value should be placed in the
then()
.

For example, let's say you want to retrieve some data from a collection and display it in a table. Since the data retrieval is an asynchronous operation, you have to wait until it is completed before using the query results to set the table's data. The

find()
function performs the query and returns a Promise. So the
then()
defines a function that runs when the query is finished. That function receives the query results and uses them to set the table's row data. Any code that follows the
then()
will run after the query has started, but before the query has finished. Therefore, it cannot use the query results.

import wixData from 'wix-data';

$w.onReady( function () {
  wixData.query("myCollection")
    .find()
    .then( (results) => {
      // this will only run once the query has returned results
      $w("#myTable").rows = results.items;
    } );
  // code here will run before the query has returned results
} );

Multiple .then( ) Calls

Sometimes, you will need to call another asynchronous function inside the

then()
of a Promise returned by another asynchronous function.

For example, let's say you want to use the

fetch()
function to retrieve data from a 3rd party service. That operation is asynchronous because it has to wait for the 3rd party to return a response. Then you want to read the response as JSON. The
json()
function of the response object is also asynchronous.

You might think that you need to nest one

then()
inside another, which would look something like this:

fetch( url, {method: 'get'} )
  .then( (response) => {
    return response.json()
      .then( (json) => {
        // do something with the json response
      } );
  } );

But because

then()
itself returns a Promise, you can call another
then()
on that returned Promise. Bottom line, that means you can chain successive
then()
calls to perform one asynchronous operation after another.

That means the code above should be written like this:

fetch( url, {method: 'get'} )
  .then( response => response.json() )
  .then( (json) => {
    // do something with the json response
  } );

Here, the

then()
on line 2 is the
then()
of the Promise returned by the
fetch()
call. And the
then()
on line 3 is the
then()
of the Promise returned by the
json()
call. The first
then()
will only run after the
fetch()
call is finished and the second
then()
will only run after the
json()
call is finished.

Returning in a .then( )

Another common misconception is what happens when you use a return inside a

then()
. Some people mistakenly expect that the value the Promise resolves to is used as the return value of the function that the
then()
is in.

For example, let's say you have a page about cars. On that page you have a
dropdown that users use to select a car model. When a selection is made, you perform a query for a specific car model and populate a table with all the matching items.

You might write some code like this thinking that the

queryCars()
function will return the car items as an array when the
find()
Promise has resolved.

import wixData from 'wix-data';

export function modelDropdown_change() {
  let model = $w("#modelDropdown").value;
  $w("#resultsTable").rows = queryCars(model);
}

// This will not work as expected
function queryCars(model) {
  wixData.query("cars")
    .eq("model", model)
    .find()
    .then( (results) => {
      return results.items;
    } );
}

However, the above code will not work as expected. Let's understand why it doesn't work and what we need to do to fix it.

When we return

results.items
, we're not returning it as the return value of the
queryCars()
function. Rather, it is returned as the return value of the arrow function passed to the
then()
. That means nothing is being returned from the
queryCars()
function. So when we call
queryCars()
to set the table's rows, we're receiving nothing in return.

To remedy this situation, we need to return the result of the Promise's

then()
(or
then()
chain). To do so, we simply add a
return
before we start the chain that created the Promise. This will return the final link in our chain.

So here our chain looks like this:

  • .query()
    - Create a query - returns a WixDataQuery object
  • .eq()
    - Modify the query - returns a WixDataQuery object
  • .find()
    - Execute the query - returns a Promise
  • .then()
    - Define what to do on resolution - returns a Promise

Adding a

return
to the start of our chain means we'll return the Promise returned from the final
then()
. Since the value returned from the
then()
is itself a Promise, we need to keep this in mind when we call the
queryCars()
function.

That means our fixed up code should look like this:

import wixData from 'wix-data';

export function modelDropdown_change() {
  let model = $w("#modelDropdown").value;
  queryCars(model)
    .then( (items) => {
      $w("#resultsTable").rows = items;
    } );
}

// The added `return` on line 13 fixes the problem
function queryCars(model) {
  return wixData.query("cars")
    .eq("model", model)
    .find()
    .then( (results) => {
      return results.items;
    } );
}

Here the

queryCars()
function returns a Promise that resolves to the items from the query results. So when we call the
queryCars()
function we have to treat it like a Promise and use a
then()
to use its resolved value to set the table's row data.

Debugging on the Backend

Calls to

console.log()
that are made from inside the
then()
Promise in backend code are not logged in the developer console unless the Promise itself is returned to the client-side code.

For example, consider the following backend code:

export function usePromise() {
  return fetch('https://site.com/api?q=query', {method: 'get'})
    .then( (response) => {
      console.log(response.status);
  } );
}

As written, if you call

usePromise
from the client-side, the message on line 3 will be logged in the Developer Console. However, if you remove the return on line 2 and call
usePromise
from the backend, the message on line 3 will not be logged in the Developer Console.

Note
You can use Site Monitoring to view the output of console messages in backend code.

Error Handling

An asynchronous process might fail for any number of reasons. For example if you call a function like

fetch()
that communicates with another server, the call might fail because of a communication error. When a Promise does not resolve successfully, it is said to reject. You can handle rejections by adding a
catch()
to the end of your
then()
chain. The
catch()
receives the value that the Promise rejects with, usually an error.

Let's return to the example of retrieving some data from a collection and
displaying it in a table. You can handle errors by adding a

catch()
like this:

import wixData from 'wix-data';

$w.onReady( function () {
  wixData.query("myCollection")
    .find()
    .then(results => $w("#myTable").rows = results.items)
    .catch(error => {
      // handle your error here
    } );
} );

async/await

Using a

then()
with a Promise allows you to define what code will run when the Promise resolves, while at the same time allowing execution to continue with whatever code comes after the
then()
. Another option for
working with functions that return Promises is simply to wait for the
Promise to resolve before moving on to the execution of any other code.
You can do so using
async/await
.

You use

await
when calling a function that returns a Promise. It pauses the execution of your code until the Promise is resolved. In order to use
await
, you need to declare that the function in which it resides is an
async
function.

Once again, let's return to the example of retrieving some data from a
collection and displaying it in a table. But instead of using a

then()
, we will use
async/await
to deal with the Promise returned by the
find()
function.

Remember, when using a

then()
, we wrote code that looked like this:

import wixData from 'wix-data';

$w.onReady( function () {
  wixData.query("myCollection")
    .find()
    .then( (results) => {
      // this will only run once the query has returned results
      $w("#myTable").rows = results.items;
    } );
  // code here will run before the query has returned results
} );

Using

async/await
, we can instead write code like this:

import wixData from 'wix-data';

$w.onReady( async function () { 
  const results = await wixData.query("myCollection").find(); 
  // the next line will run only after the query has finished
  $w("#table1").rows = results.items;
} );

Here, you can see on line 3 that the callback function passed to the

onReady()
function is declared as an
async
function. And when the
find()
function is called, it is called using
await
. So when the query runs, the execution waits until the Promise of the
find()
function resolves. Once the query is done, the query results are stored in the results variable which can then be used to set the table's row data.

Note, that when you create an async function, that function returns a Promise. So when you call that function you need to use one of the approaches discussed in this article to deal with the Promise it returns.

For example, here is an async function that queries a collection to find an item with a specified name.

import wixData from 'wix-data';

//...

async function findName(name) {
  const results = await wixData.query("myCollection")
    .eq("name", name)
    .find();
  return results.items[0];
}

You can call this function using a

then()
:

$w.onReady( function () {
  findName("Bob")
    .then(item => console.log(item));
} );

Or, you can call it using

async/await
:

$w.onReady( async function () {
  const item = await findName("Bob");
  console.log(item);
} );

Error Handling

When using

async/await
you use a regular
try/catch
block to handle any Promise rejections.

import wixData from 'wix-data';

$w.onReady( async function () { 
  try {
    const results = await wixData.query("myCollection").find(); 
    $w("#table1").rows = results.items;
  }
  catch(error) {
    // handle the error here
  }
} );

Image Credit: https://pixahive.com/photo/promise-illustration/

Previously published at https://support.wix.com/en/article/velo-working-with-promises