Senger CodeLab 🚀

Is it an anti-pattern to use asyncawait inside of a new Promise constructor

September 29, 2026

Is it an anti-pattern to use asyncawait inside of a new Promise constructor

The landscape of modern JavaScript development is deeply intertwined with asynchronous operations. Among the most powerful tools in a developer’s arsenal are async/await and the Promise constructor. While both are fundamental for handling non-blocking code, a common point of confusion and debate arises when developers consider combining them: is it an anti-pattern to use async/await inside of a new Promise() constructor? This question delves into the very core of how we manage asynchronous flows, touching upon redundancy, error handling, and code clarity. Understanding the nuances is crucial for writing efficient, maintainable, and robust JavaScript applications, ensuring that our code harnesses the power of Promises without introducing unnecessary complexity or potential pitfalls. Let’s explore why this pattern is generally discouraged and when, if ever, it might be justifiable.

Understanding Async/Await and New Promise() Fundamentals

To fully grasp why combining async/await with new Promise() can be problematic, we must first understand their individual roles. The Promise constructor, introduced in ES6, provides a way to encapsulate an asynchronous operation. It takes an “executor” function as an argument, which receives two functions: resolve and reject. These functions are called when the asynchronous operation successfully completes or encounters an error, respectively, determining the eventual state of the Promise (fulfilled or rejected).

Conversely, async/await, introduced in ES2017, is syntactic sugar built on top of Promises to make asynchronous code appear and behave more like synchronous code. An async function implicitly returns a Promise. The await keyword can only be used inside an async function and pauses the execution of that function until the awaited Promise settles (either resolves or rejects). This drastically improves readability and simplifies error handling compared to traditional callback-based approaches or explicit Promise chaining.

The primary intent of async/await is to streamline Promise-based asynchronous logic, making it easier to reason about sequential operations without deep nesting. For instance, fetching data from multiple APIs in order can be expressed much more cleanly with async/await. On the other hand, new Promise() is typically used when converting older, callback-based asynchronous APIs into modern Promise-based ones, or when creating a Promise from scratch for an operation that doesn’t inherently return one.

The “Anti-Pattern” Argument Explored

Using async/await directly inside a new Promise() constructor’s executor function is generally considered an anti-pattern because it introduces unnecessary redundancy and can complicate error handling. An async function, by its very definition, already returns a Promise. When you wrap an async function (or an await expression) inside a new Promise(), you are essentially creating a Promise to wrap another Promise, which is almost always superfluous.

Is it an anti-pattern to use async/await inside of a new Promise() constructor? Yes, in most cases, it is. This pattern, often dubbed the “Promise constructor anti-pattern,” arises because the async function already returns a Promise that you can directly await or chain with .then() and .catch(). Wrapping it inside another new Promise() constructor adds an extra, redundant layer of Promise management, making the code less direct and harder to debug. For instance, if the inner async operation rejects, and you don’t explicitly call reject() from the outer Promise’s executor, the outer Promise might remain pending indefinitely, leading to resource leaks or silent failures.

Consider the following problematic example:

function fetchDataBad() { return new Promise(async (resolve, reject) => { try { const response = await fetch('https://api.example.com/data'); const data = await response.json(); resolve(data); } catch (error) { reject(error); } }); } 

This code is redundant. The async keyword on the executor function already makes it return a Promise. The explicit new Promise() wrapper is not needed. A cleaner, more idiomatic approach would be:

async function fetchDataGood() { const response = await fetch('https://api.example.com/data'); const data = await response.json(); return data; // The async function implicitly wraps this in a Promise } 

This refactored version is not only more concise but also correctly handles errors through the natural Promise chain, making it easier to manage with try...catch blocks in the caller or with .catch() handlers. As MDN Web Docs emphasizes, “An async function can contain an await expression, that pauses the execution of the async function and waits for the passed Promise’s resolution, and then resumes the async function’s execution and returns the resolved value.” This inherent behavior means manual Promise construction is rarely necessary when async/await is already in play.

Infographic: Visualizing Async/Await vs. New Promise() Workflows
When it Might (Rarely) Make Sense ---------------------------------

While the combination of async/await inside new Promise() is largely an anti-pattern, there are extremely rare, specific edge cases where it might seem justifiable, primarily for interoperability with older, callback-based APIs that cannot be directly awaited. This typically occurs when you need to “promisify” a callback-hell function, but the callback itself performs an asynchronous operation that you wish to await before resolving the outer Promise.

For example, imagine a deeply nested legacy API that expects a callback, and inside that callback, you need to perform another asynchronous operation using modern async/await syntax before you can signal completion to the original Promise. Even in these scenarios, experienced developers often find more elegant solutions, but if forced to interact with highly restrictive legacy interfaces, this pattern might emerge.

Here’s a hypothetical scenario for bridging a legacy callback API:

  1. You have a legacy function legacyOperation(data, callback) that takes a callback.
  2. Inside the callback, you need to perform an async task (e.g., store data in a database).
  3. You want to expose legacyOperation as a Promise-based function.
function promisifyLegacyWithAsyncInside(data) { return new Promise((resolve, reject) => { legacyOperation(data, async (err, result) => { // legacyOperation expects a callback if (err) { return reject(err); } try { // Here, we use await inside the callback before resolving the outer Promise const processedResult = await someAsyncProcessing(result); resolve
<b>Question & Answer : </b><br></br><p>I'm using the async.eachLimit function to control the maximum number of operations at a time.</p> const { eachLimit } = require("async"); function myFunction() { return new Promise(async (resolve, reject) => { eachLimit((await getAsyncArray), 500, (item, callback) => { // do other things that use native promises. }, (error) => { if (error) return reject(error); // resolve here passing the next value. }); }); }  <p>As you can see, I can't declare the myFunction function as async because I don't have access to the value inside the second callback of the eachLimit function.</p>
<br></br><p>You're effectively using promises inside the promise constructor executor function, so this is the <a href="https://stackoverflow.com/questions/23803743/what-is-the-explicit-promise-construction-antipattern-and-how-do-i-avoid-it">Promise constructor anti-pattern</a>.</p> <p>Your code is a good example of the main risk: not propagating all errors safely. Read why <a href="https://stackoverflow.com/a/25569299/918910">there</a>.</p> <p>In addition, the use of async/await can make the same traps even more surprising. Compare:</p> <p></p><div class="snippet" data-babel="false" data-console="true" data-hide="false" data-lang="js"> <div class="snippet-code"> let p = new Promise(resolve => { ""(); // TypeError resolve(); }); (async () => { await p; })().catch(e => console.log("Caught: " + e)); // Catches it. </div> </div> <p></p> <p>with a naive (wrong) async equivalent:</p> <p></p><div class="snippet" data-babel="false" data-console="true" data-hide="false" data-lang="js"> <div class="snippet-code"> let p = new Promise(async resolve => { ""(); // TypeError resolve(); }); (async () => { await p; })().catch(e => console.log("Caught: " + e)); // Doesn't catch it! </div> </div> <p></p> <p>Look in your browser's web console for the last one.</p> <p>The first one works because any <em>immediate</em> exception in a Promise constructor executor function conveniently rejects the newly constructed promise (but inside any .then you're on your own).</p> <p>The second one doesn't work because any immediate exception in an async function rejects the <em>implicit promise returned by the async function itself</em>.</p> <p>Since the return value of a promise constructor executor function is unused, that's bad news!</p> <h3>Your code</h3> <p>There's no reason you can't define myFunction as async:</p> async function myFunction() { let array = await getAsyncArray(); return new Promise((resolve, reject) => { eachLimit(array, 500, (item, callback) => { // do other things that use native promises. }, error => { if (error) return reject(error); // resolve here passing the next value. }); }); }  <p>Though why use outdated concurrency control libraries when you have await?</p>