DEV Community

mrZedov
mrZedov

Posted on

💥 I Got Tired of Missing await – So I Wrote an ESLint Plugin That Forces You to Handle Promises Properly

eslint-plugin-require-await-for-promise

🧵 Backstory

I didn’t plan to write an ESLint rule.

It started with one forgotten await. Then another. Then one inside a return with a logical expression. Then a bug that only appeared in production — because a Promise silently failed.

After reviewing pull request after pull request with comments like:

“Hey, you forgot to await this.”
“You’re calling an async function but ignoring the result.”
“This may run in parallel and break stuff.”

…I finally decided to do something about it.

So I built a custom ESLint rule that enforces what teams often assume: if you call a Promise-returning function, you’d better handle it.

💥 I Got Tired of Missing await – So I Wrote an ESLint Plugin That Forces You to Handle Promises Properly

😫 Tired of Silent Failures?

You call an async function.

You forget to await.

  • No errors.
  • No warnings.
  • No stack trace.
  • Just... bugs. That’s the painful reality of JavaScript and TypeScript development with Promises.

We’ve all done it:

doSomethingAsync(); // 👋 no await, no then, no catch — and no error
Enter fullscreen mode Exit fullscreen mode

This line quietly executes. The promise silently starts. If it fails — and no one’s watching — your app may break in weird ways.


🧨 Real-World Pain

In my day-to-day work, I’ve seen these kinds of bugs:

  • Missed errors from unhandled API calls
  • Double-click bugs because async handlers weren’t awaited
  • Race conditions caused by out-of-order promise execution
  • Code that "just works" in dev but explodes in production

And in reviews, I kept commenting:

"You forgot to await this"

"Please add .catch()"

"Is this handled?"

Eventually, I snapped.


🛠️ So I Built a Rule: require-await-for-promise

Meet eslint-plugin-require-await-for-promise — a TypeScript-aware ESLint plugin that forces you to handle every Promise call.

It warns if you call any function that returns a Promise but don’t:

  • await it
  • chain .then(), .catch(), or .finally()
  • return it
  • wrap it in Promise.all, Promise.race, etc.

✅ Good Code (Handled Promises)

await fetchUser();

fetchUser().then(console.log);

return fetchUser();

await Promise.all([
  fetchUser(),
  fetchPosts()
]);

return await fetchUser() || await fetchDefaultUser();
Enter fullscreen mode Exit fullscreen mode

❌ Bad Code (Untracked Promises)

fetchUser(); // ❌ No await

return fetchUser() || fetchDefaultUser(); // ❌ Both unhandled

fetchData() || console.log('fallback'); // ❌ Even worse

return callA() || callB() || callC(); // ❌ Multiple chances to lose control

callWithoutAwait().catch(() => {}); // ✅ handled
Enter fullscreen mode Exit fullscreen mode

💡 Why This Rule Helps

This ESLint rule has saved me and my team hours of debugging. It:

  • Prevents "fire-and-forget" async calls
  • Forces you to acknowledge side effects
  • Guards against accidental Promise loss in logical expressions
  • Promotes explicit and predictable async code

It catches bugs before they reach runtime.

And it's especially useful in large codebases where async behavior is subtle, or contributors may be less experienced with Promises.


🔍 How It Works

The plugin uses @typescript-eslint/utils and the TypeScript type checker to:

  • Analyze every function call
  • Determine if it returns a Promise<...>
  • Check its surrounding context
  • Exclude safe patterns like return fetch(), Promise.all(...), etc.

It even walks up AST trees to detect logical expressions like a() || b() in return statements — a common source of bugs.


⚙️ Installation

npm install --save-dev eslint-plugin-require-await-for-promise
Enter fullscreen mode Exit fullscreen mode

Then in your ESLint config (JSON or Flat Config):

{
  "plugins": ["require-await-for-promise"],
  "rules": {
    "require-await-for-promise/require-await-for-promise": "error"
  },
  "parser": "@typescript-eslint/parser",
  "parserOptions": {
    "project": "./tsconfig.json"
  }
}
Enter fullscreen mode Exit fullscreen mode

⚠️ parserOptions.project is required so the rule can analyze types.


🧪 Want to Try It?

Here’s a complete test function you can lint:

async function test(): Promise<void> {
  delayedThing(); // ❌ Error
  await delayedThing(); // ✅ OK
  delayedThing().then(() => {}); // ✅ OK
  return delayedThing(); // ✅ OK
  return delayedThing() || delayedThing(); // ❌ Error
}
Enter fullscreen mode Exit fullscreen mode

📦 Package Links


🤝 Contributions Welcome!

Have ideas, edge cases, or want to improve this rule? Open an issue or PR on GitHub.

If this plugin saved you time — star the repo and share it with others who hate silent async bugs as much as I do.

Happy linting! 🚀

Top comments (1)

Collapse
 
vitalii_4f75e9275aa55 profile image
Vitalii

I've already downloaded this - we have exactly what we need at our company on express!
There is no autofix. But it's not a problem - everything is highlighted and you can simply fix it