SonarQube
async function
await redundancy
code optimization
programming best practices

Why is Sonarqube telling me await is redundant in async function

Master System Design with Codemia

Enhance your system design skills with over 120 practice problems, detailed solutions, and hands-on exercises.

Introduction

SonarQube flags await as redundant when an async function's last statement is return await somePromise(). Since async functions automatically wrap their return value in a Promise, return promise and return await promise produce the same result to the caller. The await adds unnecessary overhead — it unwraps the Promise then immediately re-wraps it. However, there is one important exception: inside a try/catch block, return await is necessary to catch rejections. SonarQube rule S4326 (or the ESLint equivalent no-return-await) targets this pattern.

The Redundant Pattern

javascript
1// FLAGGED: await is redundant before return
2async function getUser(id) {
3    return await fetch(`/api/users/${id}`);  // ⚠️ Redundant await
4}
5
6// FIXED: just return the promise directly
7async function getUser(id) {
8    return fetch(`/api/users/${id}`);  // ✅ Same behavior, less overhead
9}
10
11// Both are called the same way — no difference to the caller
12const user = await getUser(123);

Both versions return a Promise that resolves to the fetch result. The await in the first version unwraps the Promise, then the async function immediately wraps the result back into a Promise. This round-trip is unnecessary.

Why It Matters

javascript
1// return await: 2 microtask ticks
2async function withAwait() {
3    return await Promise.resolve(42);
4    // 1. await unwraps Promise → 42
5    // 2. async wraps 42 → new Promise(42)
6}
7
8// return: 1 microtask tick
9async function withoutAwait() {
10    return Promise.resolve(42);
11    // 1. async returns the Promise directly (optimized in V8)
12}
13
14// Performance difference is negligible in most code,
15// but the code is cleaner without the redundant await

Modern JavaScript engines (V8, SpiderMonkey) optimize away the extra microtask in many cases, so the performance difference is minimal. The main benefit of removing redundant await is cleaner, more intentional code.

When await IS Required: try/catch

javascript
1// WITHOUT await: catch block NEVER executes
2async function getUserBroken(id) {
3    try {
4        return fetch(`/api/users/${id}`);  // ❌ Promise rejection is NOT caught
5    } catch (error) {
6        console.error("Failed:", error);
7        return null;
8    }
9}
10
11// WITH await: catch block properly catches rejections
12async function getUserFixed(id) {
13    try {
14        return await fetch(`/api/users/${id}`);  // ✅ Rejection is caught
15    } catch (error) {
16        console.error("Failed:", error);
17        return null;
18    }
19}

Without await, the Promise is returned before it settles — the rejection happens outside the try/catch scope. With await, the function waits for the Promise to resolve or reject, allowing the catch block to handle errors. This is the one case where return await is necessary and correct.

SonarQube Rule Details

javascript
1// S4326: "Redundant use of await on a return value"
2// This rule flags:
3
4async function a() { return await somePromise(); }      // ⚠️ Flagged
5async function b() { return await getValue(); }          // ⚠️ Flagged
6
7// This rule does NOT flag:
8async function c() {
9    try { return await somePromise(); } catch (e) {}     // ✅ Not flagged
10}
11async function d() {
12    const result = await somePromise();                   // ✅ Not flagged (not return)
13    return result;
14}
15async function e() { await somePromise(); }              // ✅ Not flagged (no return)

SonarQube and ESLint are smart enough to not flag return await inside try/catch blocks, because the await is necessary there.

ESLint Equivalent

javascript
1// .eslintrc.json
2{
3    "rules": {
4        "no-return-await": "warn"
5    }
6}
7
8// Or with @typescript-eslint for TypeScript
9{
10    "rules": {
11        "@typescript-eslint/return-await": ["error", "in-try-catch"]
12    }
13}

The @typescript-eslint/return-await rule with "in-try-catch" is the most precise: it requires await inside try/catch and forbids it elsewhere.

Common Patterns and Fixes

javascript
1// Pattern 1: Simple return — remove await
2async function getData() {
3    return await api.get('/data');           // ⚠️
4    return api.get('/data');                 // ✅
5}
6
7// Pattern 2: Return in try/catch — keep await
8async function getData() {
9    try {
10        return await api.get('/data');       // ✅ Required for catch
11    } catch (e) {
12        return defaultData;
13    }
14}
15
16// Pattern 3: Intermediate processing — already correct
17async function getData() {
18    const raw = await api.get('/data');      // ✅ Not a return
19    return transform(raw);
20}
21
22// Pattern 4: Conditional return — remove await
23async function getData(useCache) {
24    if (useCache) {
25        return await cache.get('data');      // ⚠️
26        return cache.get('data');            // ✅
27    }
28    return await api.get('/data');           // ⚠️
29    return api.get('/data');                 // ✅
30}
31
32// Pattern 5: finally block — keep await
33async function getData() {
34    try {
35        return await api.get('/data');       // ✅ Required
36    } finally {
37        cleanup();  // Runs before the return
38    }
39}

Stack Traces Consideration

javascript
1// Without await: the async function is missing from the error stack trace
2async function outerWithout() {
3    return innerAsync();  // outerWithout not in stack trace on rejection
4}
5
6// With await: the async function appears in the stack trace
7async function outerWith() {
8    return await innerAsync();  // outerWith IS in stack trace
9}
10
11// This can matter for debugging, but V8's --async-stack-traces
12// flag (enabled by default in Node 12+) largely eliminates this issue

In older JavaScript engines, removing await causes the calling function to disappear from error stack traces. Modern engines with async stack traces have largely solved this, but it remains a consideration for some environments.

Common Pitfalls

  • Removing await inside try/catch: If you have try { return await promise; } catch (e) { ... } and remove the await, the catch block will never execute for Promise rejections. The rejection propagates to the caller instead of being handled locally.
  • Removing await inside try/finally: finally blocks execute before the returned value is resolved. Without await, the finally block runs before the Promise settles, which can cause cleanup to happen at the wrong time.
  • Blindly applying the rule to all files: Auto-fixing no-return-await across a codebase without checking for try/catch context can introduce bugs. Use @typescript-eslint/return-await with "in-try-catch" mode for safe enforcement.
  • Confusing with non-async functions: return await in a non-async function is a syntax error (you cannot use await outside async). SonarQube's rule only applies to async functions.
  • Assuming performance improvement is significant: The micro-optimization of removing one microtask tick is negligible in real applications. The primary benefit is code clarity and expressing intent — "I'm passing through this Promise" rather than "I'm awaiting and re-returning this value."

Summary

  • return await promise is redundant in async functions — just use return promise
  • The one exception: return await is required inside try/catch to catch Promise rejections
  • return await is also needed in try/finally for correct execution order
  • SonarQube rule S4326 and ESLint's no-return-await flag this pattern
  • Use @typescript-eslint/return-await with "in-try-catch" for the best enforcement
  • The performance difference is negligible — the benefit is cleaner, more intentional code

Course illustration
Course illustration

All Rights Reserved.