Async code is everywhere in our app: API calls, storage access, background tasks, test scripts, GitHub Actions, and more. This document explains how and when to use sequential vs. parallel async flows, and why our rules exist.
- Clarity: Async/await makes inherently sequential logic easier to read and review.
- Performance: Parallelizing independent work avoids unnecessary delays.
- Consistency: Shared rules make it easier for contributors inside and outside Expensify to write reliable code.
When order matters, async/await expresses intent in a clear, linear style.
Example: Upload a file → Parse it → Save results.
```ts
const uploaded = await uploadFile(file);
const parsed = await parseReceipt(uploaded.url);
await saveExpense(parsed);
```
If two operations don’t depend on each other, start them together. Here are the different ways to run them in parallel:
-
Promise.all: All must succeed, fails fast on first rejection. Use when you need every result.const [user, permissions] = await Promise.all([ getUser(), getPermissions(), ]);
-
Promise.allSettled: Wait for everything, regardless of failures. Use when you don't need all results.const results = await Promise.allSettled([syncReceipts(), syncInvoices(), syncReports()]); const succeeded = results.filter(r => r.status === 'fulfilled'); const failed = results.filter(r => r.status === 'rejected');
-
Promise.any: Return the first successful result, ignore failures until one resolves. Great for redundant sources.const fastConfig = await Promise.any([ fetchFromCDN(), fetchFromBackup(), fetchFromLocalMirror(), ]);
Components should not wait for one API call before starting another unless there is a dependency. Rendering must never be blocked by network requests.
Refer to DATA-BINDING.md for full details.
If a flow really must happen in order, write it in src/libs/ or an action/helper. The UI should call that as a single logical operation.
Use async/await unless you’re:
- Wrapping callback-based APIs.
function sleep(ms: number): Promise<void> {
return new Promise((resolve) => {
setTimeout(resolve, ms);
});
}- Creating deferred promises (signals like “ready” or “loaded”).
let resolveReady: () => void;
const isReady = new Promise<void>((resolve) => {
resolveReady = resolve;
});
// later in the code
resolveReady();Error handling style depends on context. If you’re handling a single async operation, .catch() is concise and effective.
// PREFERRED
async function getData(url: string) {
const data = await fetch(url).catch(() => fetchFallback(url));
return process(data);
}// BAD — needs an outer let just to span try/catch, adds noise and requires a mutable variable
async function getData(url: string) {
let data: DataType | undefined;
try{
data = await fetch(url)
} catch (e){
data = fetchFallback(url)
}
return process(data);
}If you need to handle multiple sequential async operations, try/catch provides cleaner flow and better readability.
async function getData(url: string) {
try {
const response = await fetch(url);
const data = await response.json();
return process(data);
} catch (error) {
const data = fetchFallback(url);
return process(data);
}
}// An async function returns a promise and can be passed to helpers expecting a promise
const dataPromise = useMemo(() => loadDashboard(), []);
use(dataPromise);