Velo: Working with Promises

Visit the Velo by Wix website to onboard and continue learning.

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:

Copy
1
console.log("1")
2
3
wixData.query("myCollection")
4
.find()
5
.then( (results) => {
6
console.log("2");
7
} );
8
9
console.log("3");

The following will be logged to the console.

Copy
1
1
2
3
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.

Copy
1
import wixData from 'wix-data';
2
3
$w.onReady( function () {
4
wixData.query("myCollection")
5
.find()
6
.then( (results) => {
7
// this will only run once the query has returned results
8
$w("#myTable").rows = results.items;
9
} );
10
// code here will run before the query has returned results
11
} );

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:

Copy
1
fetch( url, {method: 'get'} )
2
.then( (response) => {
3
return response.json()
4
.then( (json) => {
5
// do something with the json response
6
} );
7
} );

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:

Copy
1
fetch( url, {method: 'get'} )
2
.then( response => response.json() )
3
.then( (json) => {
4
// do something with the json response
5
} );

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.

Copy
1
import wixData from 'wix-data';
2
3
export function modelDropdown_change() {
4
let model = $w("#modelDropdown").value;
5
$w("#resultsTable").rows = queryCars(model);
6
}
7
8
// This will not work as expected
9
function queryCars(model) {
10
wixData.query("cars")
11
.eq("model", model)
12
.find()
13
.then( (results) => {
14
return results.items;
15
} );
16
}

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:

Copy
1
import wixData from 'wix-data';
2
3
export function modelDropdown_change() {
4
let model = $w("#modelDropdown").value;
5
queryCars(model)
6
.then( (items) => {
7
$w("#resultsTable").rows = items;
8
} );
9
}
10
11
// The added `return` on line 13 fixes the problem
12
function queryCars(model) {
13
return wixData.query("cars")
14
.eq("model", model)
15
.find()
16
.then( (results) => {
17
return results.items;
18
} );
19
}

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:

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

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:

  • Wix Editor: You can use Functional Testing or Logs to view the output of console messages in backend code.
  • Wix Studio: You can use Logs 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:

Copy
1
import wixData from 'wix-data';
2
3
$w.onReady( function () {
4
wixData.query("myCollection")
5
.find()
6
.then(results => $w("#myTable").rows = results.items)
7
.catch(error => {
8
// handle your error here
9
} );
10
} );

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:

Copy
1
import wixData from 'wix-data';
2
3
$w.onReady( function () {
4
wixData.query("myCollection")
5
.find()
6
.then( (results) => {
7
// this will only run once the query has returned results
8
$w("#myTable").rows = results.items;
9
} );
10
// code here will run before the query has returned results
11
} );

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

Copy
1
import wixData from 'wix-data';
2
3
$w.onReady( async function () {
4
const results = await wixData.query("myCollection").find();
5
// the next line will run only after the query has finished
6
$w("#table1").rows = results.items;
7
} );

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.

Copy
1
import wixData from 'wix-data';
2
3
//...
4
5
async function findName(name) {
6
const results = await wixData.query("myCollection")
7
.eq("name", name)
8
.find();
9
return results.items[0];
10
}

You can call this function using a then():

Copy
1
$w.onReady( function () {
2
findName("Bob")
3
.then(item => console.log(item));
4
} );

Or, you can call it using async/await:

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

await and .then( ) Best Practice

A common error is to use await and then() on the same asynchronous call. For example:

Copy
1
$w.onReady( async function () {
2
// Don't do this
3
const item = await findName("Bob")
4
.then((results) => {
5
return results;
6
});
7
console.log(item);
8
} );

When the promise returned by the call to findName() is resolved, the code inside the then() handler runs first. After that code executes, then() returns the results as a promise. await then waits for this promise to resolve.

In the above code then() returns a promise that resolves to the same value as the one it received, so the end result is the same, but we've added unnecessary complexity and more opportunities for confusion and error.

If our then() doesn't simply return the results of the promise, await will receive a different (and likely undefined) promise, giving us an unexpected result.

Copy
1
$w.onReady( async function () {
2
const item = await findName("Bob")
3
.then((results) => {
4
//This will log the expected value
5
console.log(results);
6
});
7
//This will log "undefined"
8
console.log(item);
9
} );

In this case, the value of item will be "undefined".

Error Handling

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

Copy
1
import wixData from 'wix-data';
2
3
$w.onReady( async function () {
4
try {
5
const results = await wixData.query("myCollection").find();
6
$w("#table1").rows = results.items;
7
}
8
catch(error) {
9
// handle the error here
10
}
11
} );

Promise.all( )

If you need to wait for multiple promises to be fulfilled before the execution of some code, you can use multiple await statements like so:

Copy
1
$w.onReady( async function () {
2
const valueA = await someFunctionA();
3
const valueB = await someFunctionB();
4
const valueC = await someFunctionC();
5
console.log(valueA + valueB + valueC);
6
} );

This code will await the resolution of each function synchronously. If each promise above takes one second to resolve, the code will take about three seconds to execute.

Promise.all() allows us to execute these functions in parallel. It accepts an iterable such as an array of promises, and if they're all fulfilled, returns a single promise that resolves to an array of the results. If even one promise rejects, then Promise.all() will immediately reject.

Promise.all() can be chained with a then() call to execute some code like so:

Copy
1
$w.onReady( async function () {
2
const valueA = someFunctionA();
3
const valueB = someFunctionB();
4
const valueC = someFunctionC();
5
6
Promise.all(valueA, valueB, valueC)
7
.then((results) => {
8
console.log(results[0] + results[1] + results[2]);
9
});
10
} );

If each promise above takes one second to resolve, the code will take about one second to execute.

Promise.all() can greatly increase efficiency when calling multiple independent functions, however, if one function needs the results of another to proceed, a sequence of awaits is still necessary.

Consider the following code:

Copy
1
$w.onReady( async function () {
2
const userID = await getUserID("Bob");
3
const orders = await getOrdersHistory(userID);
4
const purchaseStatus = await revertPurchase(orders[orders.length - 1]);
5
console.log(purchaseStatus);
6
} );

Here we are trying to revert a purchase by a user named Bob. First we need to call getUserID() with Bob's name and wait for its promise to resolve so that we can pass the resulting ID to getOrdersHistory(). Then we need to wait for the promise from getOrdersHistory() to resolve to an array of Bob's orders so that we can pass its last element to revertPurchase(). Finally, we don't want to log purchaseStatus before assigning it the results from revertPurchase(), so we need to await that promise as well.

Was this helpful?
Yes
No