diff --git a/.eslintrc.js b/.eslintrc.js index 9274a4d52d69..792bb6e89ada 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -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', diff --git a/.github/.eslintrc.js b/.github/.eslintrc.js index b0f10f4b7c9a..ef81f05c1a0d 100644 --- a/.github/.eslintrc.js +++ b/.github/.eslintrc.js @@ -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', diff --git a/contributingGuides/STYLE.md b/contributingGuides/STYLE.md index fb91a67eb7d0..07cfaa67c62a 100644 --- a/contributingGuides/STYLE.md +++ b/contributingGuides/STYLE.md @@ -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) @@ -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) @@ -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 - ## React Coding Standards ### Code Documentation diff --git a/contributingGuides/philosophies/ASYNC.md b/contributingGuides/philosophies/ASYNC.md new file mode 100644 index 000000000000..5c9ca170dd31 --- /dev/null +++ b/contributingGuides/philosophies/ASYNC.md @@ -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 { + return new Promise((resolve) => { + setTimeout(resolve, ms); + }); + } + ``` + + * Creating deferred promises (signals like “ready” or “loaded”). + + ```ts + let resolveReady: () => void; + + const isReady = new Promise((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); +``` diff --git a/contributingGuides/philosophies/INDEX.md b/contributingGuides/philosophies/INDEX.md index 7e323ad8680f..c9ada97a217e 100644 --- a/contributingGuides/philosophies/INDEX.md +++ b/contributingGuides/philosophies/INDEX.md @@ -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) diff --git a/desktop/electron-serve.ts b/desktop/electron-serve.ts index ea7a567f9eff..5999df59c091 100644 --- a/desktop/electron-serve.ts +++ b/desktop/electron-serve.ts @@ -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). @@ -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); }); diff --git a/scripts/.eslintrc.js b/scripts/.eslintrc.js index 89a094c5e7f9..da013c9eb830 100644 --- a/scripts/.eslintrc.js +++ b/scripts/.eslintrc.js @@ -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'], }, diff --git a/src/components/AttachmentPicker/index.native.tsx b/src/components/AttachmentPicker/index.native.tsx index b679a5adb805..44aa9ae06428 100644 --- a/src/components/AttachmentPicker/index.native.tsx +++ b/src/components/AttachmentPicker/index.native.tsx @@ -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 => { const pickedFiles = await pick({ type: [type === CONST.ATTACHMENT_PICKER_TYPE.IMAGE ? types.images : types.allFiles], diff --git a/src/components/FilePicker/index.native.tsx b/src/components/FilePicker/index.native.tsx index a7eb47347b08..34c6213f701f 100644 --- a/src/components/FilePicker/index.native.tsx +++ b/src/components/FilePicker/index.native.tsx @@ -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 => { const [file] = await pick({ type: [types.allFiles], @@ -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; diff --git a/src/libs/E2E/utils/NetworkInterceptor.ts b/src/libs/E2E/utils/NetworkInterceptor.ts index 2b32857b1d06..a9e7e660df3f 100644 --- a/src/libs/E2E/utils/NetworkInterceptor.ts +++ b/src/libs/E2E/utils/NetworkInterceptor.ts @@ -1,4 +1,3 @@ -/* eslint-disable @lwc/lwc/no-async-await */ import {DeviceEventEmitter} from 'react-native'; import type {NetworkCacheEntry, NetworkCacheMap} from '@libs/E2E/types'; diff --git a/tests/.eslintrc.js b/tests/.eslintrc.js index f3152bb7d2f1..98fd050b77fd 100644 --- a/tests/.eslintrc.js +++ b/tests/.eslintrc.js @@ -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'], diff --git a/tests/e2e/testRunner.ts b/tests/e2e/testRunner.ts index 6729e95c3c71..a53f63c7d664 100644 --- a/tests/e2e/testRunner.ts +++ b/tests/e2e/testRunner.ts @@ -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';