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:
- Enable the ESLint rule
prefer-promise-reject-errorsand considerno-promise-executor-returnto catch some common misuse patterns automatically. - Use the
no-floating-promisesTypeScript ESLint rule if you are on TypeScript. It catches unawaited async calls - the primary source of silent failures. - Search for
.then(in files that containasync function- these are your mixed files. Rewrite.then()/.catch()chains astry/await/catchblocks. - Audit every function that calls an async function without
await. If it is not intentionally fire-and-forget, addawaitand 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.