Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,7 @@ module.exports = {
},
rules: {
// TypeScript specific rules
'@lwc/lwc/no-async-await': 'off',
'@typescript-eslint/prefer-enum-initializers': 'error',
'@typescript-eslint/no-var-requires': 'off',
'@typescript-eslint/no-non-null-assertion': 'error',
Expand Down
2 changes: 0 additions & 2 deletions .github/.eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,6 @@ module.exports = {
rules: {
// For all these Node.js scripts, we do not want to disable `console` statements
'no-console': 'off',

'@lwc/lwc/no-async-await': 'off',
'no-await-in-loop': 'off',
'no-restricted-syntax': ['error', 'ForInStatement', 'LabeledStatement', 'WithStatement'],
'no-continue': 'off',
Expand Down
12 changes: 8 additions & 4 deletions contributingGuides/STYLE.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
- [Functions](#functions)
- [`var`, `const` and `let`](#var-const-and-let)
- [Object / Array Methods](#object--array-methods)
- [Asynchronous code](#asynchronous-code)
- [Accessing Object Properties and Default Values](#accessing-object-properties-and-default-values)
- [JSDocs](#jsdocs)
- [Component props](#component-props)
Expand Down Expand Up @@ -858,6 +859,13 @@ const modifiedArray = _.chain(someArray)
const modifiedArray = someArray.filter(filterFunc).map(mapFunc);
```

## Asynchronous code

- Prefer `async/await` over `.then/.catch` across the codebase. Use raw `Promise` only when wrapping callbacks or implementing deferred signals.
- Use `Promise.all(...)` (or `Promise.allSettled(...)`) for work that can run in parallel.
- In UI code, start unrelated async tasks at the same time (don’t wait for one to finish before starting another), and let the UI render immediately with loading states ([see data-binding](./philosophies/DATA-BINDING.md)).
- See the detailed [async philosophy document](./philosophies/ASYNC.md)

## Accessing Object Properties and Default Values

Use optional chaining (`?.`) to safely access object properties and nullish coalescing (`??`) to short circuit null or undefined values that are not guaranteed to exist in a consistent way throughout the codebase. Don't use the `lodashGet()` function. eslint: [`no-restricted-imports`](https://eslint.org/docs/latest/rules/no-restricted-imports)
Expand Down Expand Up @@ -1006,10 +1014,6 @@ JavaScript is always changing. We are excited whenever it does! However, we tend

So, if a new language feature isn't something we have agreed to support it's off the table. Sticking to just one way to do things reduces cognitive load in reviews and also makes sure our knowledge of language features progresses at the same pace. If a new language feature will cause considerable effort for everyone to adapt to or we're just not quite sold on the value of it yet, we won't support it.

Here are a couple of things we would ask that you *avoid* to help maintain consistency in our codebase:

- **Async/Await** - Use the native `Promise` instead
Comment thread
blazejkustra marked this conversation as resolved.

## React Coding Standards

### Code Documentation
Expand Down
136 changes: 136 additions & 0 deletions contributingGuides/philosophies/ASYNC.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
### Async vs. Sync Code

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.

## Why this matters

- 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.

## Rules

### - Sequential flows SHOULD use async/await
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);
```

### - Independent steps MUST be run in parallel
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.

```ts
const [user, permissions] = await Promise.all([
getUser(),
getPermissions(),
]);
```

- **`Promise.allSettled`**: Wait for everything, regardless of failures. Use when you don't need all results.

```ts
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.

```ts
const fastConfig = await Promise.any([
fetchFromCDN(),
fetchFromBackup(),
fetchFromLocalMirror(),
]);
```

### - UI SHOULD launch independent async calls in parallel
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](./DATA-BINDING.md) for full details.

### - Sequential logic SHOULD be encapsulated outside the UI
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.


### - `async/await `SHOULD be preferred over `.then/.catch`
Use `async/await` unless you’re:

* Wrapping callback-based APIs.

```ts
function sleep(ms: number): Promise<void> {
return new Promise((resolve) => {
setTimeout(resolve, ms);
});
}
```

* Creating deferred promises (signals like “ready” or “loaded”).

```ts
let resolveReady: () => void;

const isReady = new Promise<void>((resolve) => {
resolveReady = resolve;
});

// later in the code
resolveReady();
```

## More Examples

### 1) Error handling SHOULD use `.catch()`

Error handling style depends on context. If you’re handling a single async operation, `.catch()` is concise and effective.

```ts
// PREFERRED
async function getData(url: string) {
const data = await fetch(url).catch(() => fetchFallback(url));
return process(data);
}
```

```ts
// 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.

```ts
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);
}
}
```

### 2) Passing promises to `use` and other helpers

```ts
// An async function returns a promise and can be passed to helpers expecting a promise
const dataPromise = useMemo(() => loadDashboard(), []);
use(dataPromise);
```
Comment thread
blazejkustra marked this conversation as resolved.
1 change: 1 addition & 0 deletions contributingGuides/philosophies/INDEX.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ The key words "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", "SHOULD", "S
## Contents
* [Cross-Platform Philosophy](/contributingGuides/philosophies/CROSS-PLATFORM.md)
* [Data Flow Philosophy](/contributingGuides/philosophies/DATA-FLOW.md)
* [Async vs. Sync Code](/contributingGuides/philosophies/ASYNC.md)
* [Data Binding Philosophy](/contributingGuides/philosophies/DATA-BINDING.md)
* [Deploying Philosophy](/contributingGuides/philosophies/DEPLOYING.md)
* [Directory Structure and File Naming Philosophy](/contributingGuides/philosophies/DIRECTORIES.md)
Expand Down
3 changes: 1 addition & 2 deletions desktop/electron-serve.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,6 @@

/* eslint-disable rulesdir/no-negated-variables */

/* eslint-disable @lwc/lwc/no-async-await */

/**
* This file is a modified version of the electron-serve package.
* We keep the same interface, but instead of file protocol we use buffer protocol (with support of JS self profiling).
Expand Down Expand Up @@ -97,6 +95,7 @@ export default function electronServe(options: ServeOptions) {
app.on('ready', () => {
const partitionSession = mandatoryOptions.partition ? session.fromPartition(mandatoryOptions.partition) : session.defaultSession;

// eslint-disable-next-line deprecation/deprecation
partitionSession.protocol.registerBufferProtocol(mandatoryOptions.scheme, handler);
});

Expand Down
1 change: 0 additions & 1 deletion scripts/.eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ module.exports = {
// For all these Node.js scripts, we do not want to disable `console` statements
'no-console': 'off',
'no-continue': 'off',
'@lwc/lwc/no-async-await': 'off',
'no-await-in-loop': 'off',
'no-restricted-syntax': ['error', 'ForInStatement', 'LabeledStatement', 'WithStatement'],
},
Expand Down
1 change: 0 additions & 1 deletion src/components/AttachmentPicker/index.native.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -222,7 +222,6 @@ function AttachmentPicker({
/**
* Launch the DocumentPicker. Results are in the same format as ImagePicker
*/
// eslint-disable-next-line @lwc/lwc/no-async-await
const showDocumentPicker = useCallback(async (): Promise<LocalCopy[]> => {
const pickedFiles = await pick({
type: [type === CONST.ATTACHMENT_PICKER_TYPE.IMAGE ? types.images : types.allFiles],
Expand Down
2 changes: 0 additions & 2 deletions src/components/FilePicker/index.native.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,6 @@ function FilePicker({children}: FilePickerProps) {
*
* @param files The array of DocumentPickerResponse
*/
// eslint-disable-next-line @lwc/lwc/no-async-await
const pickFile = async (): Promise<LocalCopy> => {
const [file] = await pick({
type: [types.allFiles],
Expand Down Expand Up @@ -115,7 +114,6 @@ function FilePicker({children}: FilePickerProps) {
* @param onPickedHandler A callback that will be called with the selected file
* @param onCanceledHandler A callback that will be called if the file is canceled
*/
// eslint-disable-next-line @lwc/lwc/no-async-await
const open = (onPickedHandler: (file: FileObject) => void, onCanceledHandler: () => void = () => {}) => {
completeFileSelection.current = onPickedHandler;
onCanceled.current = onCanceledHandler;
Expand Down
1 change: 0 additions & 1 deletion src/libs/E2E/utils/NetworkInterceptor.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
/* eslint-disable @lwc/lwc/no-async-await */
import {DeviceEventEmitter} from 'react-native';
import type {NetworkCacheEntry, NetworkCacheMap} from '@libs/E2E/types';

Expand Down
1 change: 0 additions & 1 deletion tests/.eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ module.exports = {
extends: ['plugin:testing-library/react'],
rules: {
'no-import-assign': 'off',
'@lwc/lwc/no-async-await': 'off',
'no-await-in-loop': 'off',
'no-restricted-syntax': ['error', 'ForInStatement', 'LabeledStatement', 'WithStatement'],

Expand Down
2 changes: 1 addition & 1 deletion tests/e2e/testRunner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
* node tests/e2e/merge.js
*/

/* eslint-disable @lwc/lwc/no-async-await,no-restricted-syntax,no-await-in-loop */
/* eslint-disable no-restricted-syntax,no-await-in-loop */
import {execSync} from 'child_process';
import fs from 'fs';
import type {TestResult} from '@libs/E2E/client';
Expand Down
Loading