Mixed Async Patterns in JavaScript: .then() vs async/await in AI Code - VibeDoctor 
← All Articles 🧹 Code Quality Medium

Mixed Async Patterns in JavaScript: .then() vs async/await in AI Code

AI tools mix .then()/.catch() with async/await in the same file. Learn why this causes bugs and how to standardize your async code.

QUA-005

Quick Answer

Mixing .then()/.catch() and async/await in the same file creates inconsistent error handling, confusing control flow, and subtle bugs - especially when developers forget that .then() chains and await expressions behave differently around rejection and return values. Pick one style per project and apply it consistently. For new code, prefer async/await.

Why AI Tools Generate Mixed Async Code

When you build with Cursor, Bolt, Lovable, or v0, each prompt generates code in isolation. One prompt builds your API call with async/await. A later prompt adds a feature using .then()/.catch() because that example happened to be in the training data. A third prompt wraps both in another async function. Before long, a single file reads like three different developers wrote it - because in a sense, three different AI contexts did.

According to the Stack Overflow Developer Survey 2024, JavaScript is the most-used language for the 12th consecutive year, with async programming patterns being among the top sources of bugs and confusion. The survey found that async/await is now the dominant style, but legacy .then() chains persist heavily in AI-generated codebases because AI models are trained on older code.

SonarSource's static analysis data shows that inconsistent async patterns are flagged in approximately 28% of JavaScript files containing async operations. When those inconsistencies exist in the same file, the bug rate triples compared to consistently-styled code.

What Mixed Async Code Actually Looks Like

Here is a realistic example of what Cursor or Bolt might produce across multiple prompts in a Next.js API route:

// ❌ BAD - Mixed async patterns in one file

// Style 1: .then()/.catch() for the database call
function getUserData(userId) {
  return db.users.findUnique({ where: { id: userId } })
    .then(user => {
      return user;
    })
    .catch(err => {
      console.error(err);
      return null;
    });
}

// Style 2: async/await for the API call added later
async function fetchUserProfile(userId) {
  const user = await getUserData(userId);
  const profile = await fetch(`/api/profiles/${userId}`)
    .then(res => res.json())   // .then() mixed INTO an await expression
    .catch(() => null);
  return { user, profile };
}

// Style 3: Callback buried inside a .then() chain
async function syncUserToStripe(userId) {
  getUserData(userId).then(user => {
    stripe.customers.create({ email: user.email }, (err, customer) => {
      if (err) console.error(err);
      db.users.update({ where: { id: userId }, data: { stripeId: customer.id } })
        .then(() => console.log('synced'))
        .catch(console.error);
    });
  });
  // No return, no await - fire and forget, silently
}

The third function is particularly dangerous. It returns nothing, so the caller has no way to know if the Stripe sync succeeded. Any error in the callback is swallowed silently. The database update inside the callback is not awaited.

The Correct Approach: Consistent async/await

// ✅ GOOD - Consistent async/await throughout

async function getUserData(userId) {
  try {
    const user = await db.users.findUnique({ where: { id: userId } });
    return user;
  } catch (err) {
    console.error('getUserData failed:', err);
    return null;
  }
}

async function fetchUserProfile(userId) {
  const user = await getUserData(userId);
  let profile = null;
  try {
    const res = await fetch(`/api/profiles/${userId}`);
    profile = await res.json();
  } catch (err) {
    console.error('fetchUserProfile failed:', err);
  }
  return { user, profile };
}

async function syncUserToStripe(userId) {
  const user = await getUserData(userId);
  if (!user) throw new Error(`User ${userId} not found`);

  const customer = await stripe.customers.create({ email: user.email });
  await db.users.update({
    where: { id: userId },
    data: { stripeId: customer.id }
  });
  console.log('synced:', userId);
}

Every async operation is awaited. Errors are caught in explicit try/catch blocks. The function returns a value the caller can depend on. Failures are visible, not silent.

Three Specific Bugs Mixed Patterns Cause

It is easy to dismiss this as a style issue. It is not. Mixed async patterns cause real bugs:

1. Unhandled promise rejections. When you mix .then() and await in the same chain, a rejection in the .then() portion may not be caught by the surrounding try/catch. Node.js will emit an UnhandledPromiseRejectionWarning - and in newer versions, crash the process entirely.

2. Silent fire-and-forget. A common mistake in Bolt-generated code is calling an async function inside a .then() callback without await. The inner async operation starts, the outer chain continues, and any failure disappears without a trace. This is how data mutations get lost in production.

3. Race conditions from mixed return timing. A function that sometimes returns a resolved promise (via .then()) and sometimes an async result can behave differently depending on microtask queue ordering. These bugs are notoriously difficult to reproduce.

How to Fix Mixed Async Patterns Across a Codebase

Fixing a mixed-async codebase by hand is tedious but straightforward. The steps:

  1. Enable the ESLint rule prefer-promise-reject-errors and consider no-promise-executor-return to catch some common misuse patterns automatically.
  2. Use the no-floating-promises TypeScript ESLint rule if you are on TypeScript. It catches unawaited async calls - the primary source of silent failures.
  3. Search for .then( in files that contain async function - these are your mixed files. Rewrite .then()/.catch() chains as try/await/catch blocks.
  4. Audit every function that calls an async function without await. If it is not intentionally fire-and-forget, add await and propagate the error.

Tools like VibeDoctor (vibedoctor.io) automatically scan your codebase for mixed async patterns and flag specific file paths and line numbers. Free to sign up.

When .then() Is Acceptable

There are legitimate cases for .then() in modern JavaScript. Stream processing, chaining independent transforms, and library APIs that return non-awaitable thenables are all reasonable. The rule is consistency: if a file uses .then() chains throughout, keep it consistent. Do not mix styles within the same function or within closely related functions in the same module.

Pattern Readability Error handling Stack traces Recommended
async/await only High try/catch blocks Clear, full stack Yes
.then()/.catch() only Medium Chained .catch() Often truncated Legacy code only
Mixed in same function Low Inconsistent, gaps Confusing Never
await + inline .then() Low Unpredictable Hard to trace Never

FAQ

Is mixing .then() and async/await actually a bug or just a style issue?

Both. It is a style issue that frequently produces bugs. Inconsistent patterns lead to missed rejections, fire-and-forget mutations, and race conditions. Style rules exist precisely to prevent these error categories.

Does ESLint catch mixed async patterns?

Standard ESLint rules do not flag this directly, but the TypeScript ESLint plugin's no-floating-promises and no-misused-promises rules catch the most dangerous class of errors. Add prefer-async-await via eslint-plugin-unicorn for full coverage.

Why does Cursor keep generating .then() even when the rest of my code uses async/await?

AI models generate code based on the example in the prompt context. If your prompt or the surrounding code includes .then(), the model will mirror that style. Include explicit instructions like "use async/await throughout" in your system prompt or Cursor rules file.

Can I convert .then() chains to async/await automatically?

Yes. The VS Code extension "Convert to async/await" and the npm package convert-promise-to-await can handle many cases. For complex chains with branching logic, manual review is still required.

Does this apply to React components?

Yes. In React, mixing async patterns inside useEffect is especially problematic because the component may unmount before a .then() callback runs, triggering state updates on unmounted components. Stick to async/await inside effects with proper cleanup.

Scan your codebase for this issue - free

VibeDoctor checks for QUA-005 and 128 other issues across 15 diagnostic areas.

SCAN MY APP →
← Back to all articles View all 129+ checks →