Tutorials

Async, Await, and Promises in Retool Explained

OTC Team··4 min read
Async, Await, and Promises in Retool Explained

If you've ever triggered a query inside a Retool JS query and found that data comes back empty, or that your console.log fires before the API even responds, you've run into the async problem. Using async, await, and promises in Retool is the key to controlling execution order and actually doing something useful with query return values. Here's a clear breakdown of how it all works.

Why Does Asynchronous Code Break Things in Retool?

By default, JavaScript runs synchronously — line by line, in order. But many operations in Retool are asynchronous, meaning they kick off in the background and the rest of your code keeps running without waiting for them to finish. This includes:

  • Query triggers (e.g., myQuery.trigger())
  • Model updates like setValue
  • Utility methods like utils.table.getDisplayedData()

When you iterate over an array and trigger a REST API query for each element without await, your JS query completes, your console.log fires, and your data variable is empty — all before the API has responded even once. The triggered queries finish last, long after your code has moved on.

How to Use await to Control Query Execution Order

The fix is straightforward: use the await keyword before any async operation so your code pauses until that operation resolves. Since Retool JS queries already run inside an async context, you can use await directly without wrapping everything yourself.

Here's the pattern for triggering a query and using its return value:

  • Add await before myQuery.trigger()
  • Assign the result to a variable: const result = await myQuery.trigger(...)
  • Use result in the lines that follow — it will contain the actual API response

With await in place, each triggered query runs to completion before the next line executes. Your logs appear in order, and your data variable holds real values instead of empty objects.

Important: If you define your own inner functions inside a JS query and call async operations inside them, you must declare those functions as async explicitly. The outer Retool query function is async, but nested functions are not by default.

Why You Should Use map Instead of forEach for Async Loops

A common mistake when looping over an array of async operations is using forEach. The problem: forEach does not return values, so you can't collect the results of each async call. Use map instead — it returns an array of promises that you can then resolve all at once.

Here's the core pattern:

  • Use .map() to iterate and trigger a query for each element, collecting the returned promises into an array
  • Pass that array to Promise.all() to wait for every promise to resolve
  • The result of await Promise.all(promisesArray) is a plain array of all the API responses, ready to use in your app

This is the most reliable pattern for running multiple async queries in parallel and collecting all the results. It's faster than awaiting each call one at a time in a loop, and it gives you a single array of data to bind to a Table or pass to another query.

A Practical Step-by-Step: Fetch Multiple API Results with Promise.all

Here's how to apply this pattern end-to-end in a Retool JS query:

  • Step 1: Define your array of inputs — for example, an array of IDs or delay values: const items = [1, 2, 3]
  • Step 2: Use .map() to trigger your query for each item and collect promises: const promises = items.map(item => myQuery.trigger({ additionalScope: { item } }))
  • Step 3: Resolve all promises in parallel: const results = await Promise.all(promises)
  • Step 4: Return or use results — it's now a flat array of each query's response data, bindable anywhere in your Retool app

The Hidden Gotcha: Retool's Outer JS Query Returns Early

One thing that catches a lot of Retool developers off guard: the outer JS query function returns as soon as all its interior code has been dispatched — not when everything has finished executing. This means the onSuccess event handler can fire before any of your async operations have actually completed.

This makes chaining JS queries with async code unreliable if you rely on onSuccess alone. The solution is to keep your async logic self-contained using await and Promise.all within the same query, rather than splitting it across multiple queries connected by event handlers.

Quick Reference: What's Async in Retool

Keep this list in mind whenever you write a JS query. All of the following are async and need await if you want to use their return values:

  • query.trigger() — any REST, database, or JS query trigger
  • component.setValue() — model updates on any component
  • utils.table.getDisplayedData() — utility methods that read component state

Final Thoughts

Async JavaScript in Retool trips up even experienced developers because the default mental model — code runs top to bottom — breaks the moment you trigger a query. Once you internalize that query triggers are promises, and that await and Promise.all are your tools for controlling them, the pattern becomes second nature. Start with the map + Promise.all pattern for any multi-query loop and you'll avoid the most common pitfalls immediately.

Ready to build?

We scope, design, and ship your Retool app — fast.

Ready to ship your first tool?