return-await
Enforce consistent awaiting of returned promises.
Extending "plugin:@typescript-eslint/strict-type-checked"
in an ESLint configuration enables this rule.
Some problems reported by this rule are automatically fixable by the --fix
ESLint command line option.
Some problems reported by this rule are manually fixable by editor suggestions.
This rule requires type information to run.
This rule builds on top of the eslint/no-return-await
rule.
It expands upon the base rule to add support for optionally requiring return await
in certain cases.
The extended rule is named return-await
instead of no-return-await
because the extended rule can enforce the positive or the negative. Additionally, while the core rule is now deprecated, the extended rule is still useful in many contexts:
- Returning an awaited promise improves stack trace information.
- When the
return
statement is intry...catch
, awaiting the promise also allows the promise's rejection to be caught instead of leaving the error to the caller. - Contrary to popular belief,
return await promise;
is at least as fast as directly returning the promise.
How to Use
- Flat Config
- Legacy Config
export default tseslint.config({
rules: {
// Note: you must disable the base rule as it can report incorrect errors
"no-return-await": "off",
"@typescript-eslint/return-await": "error"
}
});
module.exports = {
"rules": {
// Note: you must disable the base rule as it can report incorrect errors
"no-return-await": "off",
"@typescript-eslint/return-await": "error"
}
};
Try this rule in the playground ↗
Options
See eslint/no-return-await
's options.
type Options =
| 'in-try-catch'
| 'always'
| 'error-handling-correctness-only'
| 'never';
const defaultOptions: Options = 'in-try-catch';
The options in this rule distinguish between "ordinary contexts" and "error-handling contexts". An error-handling context is anywhere where returning an unawaited promise would cause unexpected control flow regarding exceptions/rejections. See detailed examples in the sections for each option.
- If you return a promise within a
try
block, it should be awaited in order to trigger subsequentcatch
orfinally
blocks as expected. - If you return a promise within a
catch
block, and there is afinally
block, it should be awaited in order to trigger thefinally
block as expected. - If you return a promise between a
using
orawait using
declaration and the end of its scope, it should be awaited, since it behaves equivalently to code wrapped in atry
block followed by afinally
.
Ordinary contexts are anywhere else a promise may be returned. The choice of whether to await a returned promise in an ordinary context is mostly stylistic.
With these terms defined, the options may be summarized as follows:
Option | Ordinary Context (stylistic preference 🎨) | Error-Handling Context (catches bugs 🐛) | Should I use this option? |
---|---|---|---|
always | return await promise; | return await promise; | ✅ Yes! |
in-try-catch | return promise; | return await promise; | ✅ Yes! |
error-handling-correctness-only | don't care 🤷 | return await promise; | 🟡 Okay to use, but the above options would be preferable. |
never | return promise; | return promise; (⚠️ This behavior may be harmful ⚠️) | ❌ No. This option is deprecated. |
in-try-catch
In error-handling contexts, the rule enforces that returned promises must be awaited. In ordinary contexts, the rule enforces that returned promises must not be awaited.
This is a good option if you prefer the shorter return promise
form for stylistic reasons, wherever it's safe to use.
Examples of code with in-try-catch
:
- ❌ Incorrect
- ✅ Correct
async function invalidInTryCatch1() {
try {
return Promise.reject('try');
} catch (e) {
// Doesn't execute due to missing await.
}
}
async function invalidInTryCatch2() {
try {
throw new Error('error');
} catch (e) {
// Unnecessary await; rejections here don't impact control flow.
return await Promise.reject('catch');
}
}
// Prints 'starting async work', 'cleanup', 'async work done'.
async function invalidInTryCatch3() {
async function doAsyncWork(): Promise<void> {
console.log('starting async work');
await new Promise(resolve => setTimeout(resolve, 1000));
console.log('async work done');
}
try {
throw new Error('error');
} catch (e) {
// Missing await.
return doAsyncWork();
} finally {
console.log('cleanup');
}
}
async function invalidInTryCatch4() {
try {
throw new Error('error');
} catch (e) {
throw new Error('error2');
} finally {
// Unnecessary await; rejections here don't impact control flow.
return await Promise.reject('finally');
}
}
async function invalidInTryCatch5() {
return await Promise.resolve('try');
}
async function invalidInTryCatch6() {
return await 'value';
}
async function invalidInTryCatch7() {
using x = createDisposable();
return Promise.reject('using in scope');
}
Open in Playgroundasync function validInTryCatch1() {
try {
return await Promise.reject('try');
} catch (e) {
// Executes as expected.
}
}
async function validInTryCatch2() {
try {
throw new Error('error');
} catch (e) {
return Promise.reject('catch');
}
}
// Prints 'starting async work', 'async work done', 'cleanup'.
async function validInTryCatch3() {
async function doAsyncWork(): Promise<void> {
console.log('starting async work');
await new Promise(resolve => setTimeout(resolve, 1000));
console.log('async work done');
}
try {
throw new Error('error');
} catch (e) {
return await doAsyncWork();
} finally {
console.log('cleanup');
}
}
async function validInTryCatch4() {
try {
throw new Error('error');
} catch (e) {
throw new Error('error2');
} finally {
return Promise.reject('finally');
}
}
async function validInTryCatch5() {
return Promise.resolve('try');
}
async function validInTryCatch6() {
return 'value';
}
async function validInTryCatch7() {
using x = createDisposable();
return await Promise.reject('using in scope');
}
Open in Playgroundalways
Requires that all returned promises be awaited.
This is a good option if you like the consistency of simply always awaiting promises, or prefer not having to consider the distinction between error-handling contexts and ordinary contexts.
Examples of code with always
:
- ❌ Incorrect
- ✅ Correct
async function invalidAlways1() {
try {
return Promise.resolve('try');
} catch (e) {}
}
async function invalidAlways2() {
return Promise.resolve('try');
}
async function invalidAlways3() {
return await 'value';
}
Open in Playgroundasync function validAlways1() {
try {
return await Promise.resolve('try');
} catch (e) {}
}
async function validAlways2() {
return await Promise.resolve('try');
}
async function validAlways3() {
return 'value';
}
Open in Playgrounderror-handling-correctness-only
In error-handling contexts, the rule enforces that returned promises must be awaited. In ordinary contexts, the rule does not enforce any particular behavior around whether returned promises are awaited.
This is a good option if you only want to benefit from rule's ability to catch control flow bugs in error-handling contexts, but don't want to enforce a particular style otherwise.
We recommend you configure either in-try-catch
or always
instead of this option.
While the choice of whether to await promises outside of error-handling contexts is mostly stylistic, it's generally best to be consistent.
Examples of additional correct code with error-handling-correctness-only
:
- ✅ Correct
async function asyncFunction(): Promise<void> {
if (Math.random() < 0.5) {
return await Promise.resolve();
} else {
return Promise.resolve();
}
}
Open in Playgroundnever
Disallows awaiting any returned promises.
This option is deprecated and will be removed in a future major version of typescript-eslint.
The never
option introduces undesirable behavior in error-handling contexts.
If you prefer to minimize returning awaited promises, consider instead using in-try-catch
instead, which also generally bans returning awaited promises, but only where it is safe not to await a promise.
See more details at typescript-eslint#9433.
Examples of code with never
:
- ❌ Incorrect
- ✅ Correct
async function invalidNever1() {
try {
return await Promise.resolve('try');
} catch (e) {}
}
async function invalidNever2() {
return await Promise.resolve('try');
}
async function invalidNever3() {
return await 'value';
}
Open in Playgroundasync function validNever1() {
try {
return Promise.resolve('try');
} catch (e) {}
}
async function validNever2() {
return Promise.resolve('try');
}
async function validNever3() {
return 'value';
}
Open in PlaygroundWhen Not To Use It
Type checked lint rules are more powerful than traditional lint rules, but also require configuring type checked linting.
See Troubleshooting > Linting with Type Information > Performance if you experience performance degradations after enabling type checked rules.