阿里云主机折上折
  • 微信号
Current Site:Index > Asynchronous pitfalls: the confusing behaviors of Promise and async/await

Asynchronous pitfalls: the confusing behaviors of Promise and async/await

Author:Chuan Chen 阅读数:13942人阅读 分类: 前端综合

Promises and async/await are core tools for modern JavaScript asynchronous programming, but some of their behaviors can often be confusing. From the priority of microtask queues to hidden pitfalls in error handling, these features conceal many counterintuitive details.

The "Immediate Execution" Illusion of Promise.resolve

console.log('Script start');
Promise.resolve().then(() => console.log('Promise 1'));
setTimeout(() => console.log('setTimeout'), 0);
console.log('Script end');

// Output order:
// Script start
// Script end
// Promise 1
// setTimeout

Promise callbacks are placed in the microtask queue, while setTimeout callbacks enter the macrotask queue. The event loop always empties the microtask queue before processing macrotasks, even if setTimeout's delay is set to 0. This priority difference can lead to unexpected execution orders.

More subtly, even if a Promise is already in a resolved state, its then callback will still be executed asynchronously:

const p = Promise.resolve(42);
p.then(v => console.log(v)); // Still executes asynchronously
console.log('Synchronous code');

The Return Value Trap of Async Functions

Async functions always return Promises, but there are several special scenarios for return value handling:

async function foo() {
  return 123; // Equivalent to return Promise.resolve(123)
}

async function bar() {
  return Promise.resolve(456); // Note: This creates a double Promise
}

const result = bar();
result.then(v => console.log(v)); // Outputs 456, but undergoes two unwrappings

When returning a thenable object, the behavior becomes more complex:

async function baz() {
  return {
    then(resolve) {
      resolve('Manual thenable');
    }
  };
}

baz().then(console.log); // Outputs "Manual thenable"

The "Black Hole" Phenomenon of Error Handling

Uncaught Promise rejections can lead to silent failures:

function riskyOperation() {
  return new Promise((_, reject) => {
    setTimeout(() => reject(new Error('Failure')), 1000);
  });
}

// No .catch handler
riskyOperation(); // The error will be silently swallowed

In Node.js environments, this may trigger the unhandledRejection event. Browser consoles typically display warnings but won't interrupt program execution.

Error handling with async/await also has its own quirks:

async function fetchData() {
  try {
    const res = await fetch('invalid-url');
    return await res.json();
  } catch (e) {
    console.log('Caught error:', e);
    throw e; // If not rethrown, the caller won't know an error occurred
  }
}

// The caller still needs to handle the error
fetchData().catch(e => console.log('External catch:', e));

Value Penetration in Promise Chains

Returning non-Promise values in then callbacks leads to value penetration:

Promise.resolve(1)
  .then(x => x + 1) // Returns 2
  .then(x => { /* No return, equivalent to returning undefined */ })
  .then(x => console.log(x)); // Outputs undefined

But if a Promise is returned in then, it will wait for that Promise to resolve:

Promise.resolve(1)
  .then(x => new Promise(r => setTimeout(() => r(x + 1), 1000)))
  .then(console.log); // 2 (after 1 second)

Async/Await and Parallel Execution

A common misuse is over-serializing asynchronous operations:

// Inefficient approach
async function slowFetch() {
  const a = await fetch('/api/a');
  const b = await fetch('/api/b'); // Waits for a to complete before starting
  return [a, b];
}

// Correct parallel approach
async function fastFetch() {
  const aPromise = fetch('/api/a');
  const bPromise = fetch('/api/b');
  return Promise.all([aPromise, bPromise]);
}

But error handling in parallel execution requires special attention:

async function parallelWithError() {
  try {
    const [a, b] = await Promise.all([
      fetch('/api/a'),
      fetch('/api/b').then(() => Promise.reject('Intentional error'))
    ]);
  } catch (e) {
    console.log('Caught error:', e); // Triggered if any Promise rejects
  }
}

Immediate Execution of Promise Constructors

The Promise constructor immediately executes the executor function:

console.log('Start');
new Promise(resolve => {
  console.log('Inside executor');
  resolve();
});
console.log('End');

// Output order:
// Start
// Inside executor
// End

This feature is often used to wrap callback APIs:

function delay(ms) {
  return new Promise(resolve => setTimeout(resolve, ms));
}

delay(1000).then(() => console.log('After 1 second'));

Multiple Then Callbacks Issue

A Promise can have multiple then callbacks attached:

const p = Promise.resolve('Data');
p.then(v => console.log('Callback 1:', v));
p.then(v => console.log('Callback 2:', v)); // Also executes

But if a then callback throws an error, it won't affect other callbacks:

const p = Promise.resolve('Safe');
p.then(() => { throw new Error('Boom!') });
p.then(() => console.log('Still executes')); // This line runs normally

Implicit Returns in Async Functions

An async function without a return implicitly returns a Promise resolved to undefined:

async function noReturn() {
  // No return statement
}

noReturn().then(v => console.log(v)); // Outputs undefined

Even if the function throws an error, it returns a rejected Promise rather than throwing directly:

async function throwsError() {
  throw new Error('Internal error');
}

// Needs to be caught at the call site
throwsError().catch(e => console.log('Caught:', e));

Early Termination Trap in Promise.race

Promise.race returns as soon as the first Promise settles, but other Promises continue executing:

function createTask(id, delay) {
  return new Promise(resolve => {
    setTimeout(() => {
      console.log(`Task ${id} completed`);
      resolve(id);
    }, delay);
  });
}

Promise.race([
  createTask(1, 1000),
  createTask(2, 2000)
]).then(winner => console.log(`Winner: ${winner}`));

// Output:
// Task 1 completed
// Winner: 1
// Task 2 completed (Still executes)

Recursive Microtask Queue Explosion

Recursively adding microtasks can block the event loop:

function recursiveMicrotask(count = 0) {
  if (count >= 100000) return;
  Promise.resolve().then(() => {
    console.log(`Microtask ${count}`);
    recursiveMicrotask(count + 1);
  });
}

recursiveMicrotask(); // May freeze the browser

In contrast, recursion with setTimeout is "friendly":

function recursiveMacrotask(count = 0) {
  if (count >= 100) return;
  setTimeout(() => {
    console.log(`Macrotask ${count}`);
    recursiveMacrotask(count + 1);
  }, 0);
}

recursiveMacrotask(); // Executes in chunks

Awaiting Non-Promises

await can wait for any value; non-Promise values are automatically wrapped:

async function awaitNonPromise() {
  const v = await 42; // Equivalent to await Promise.resolve(42)
  console.log(v); // 42
}

But awaiting thenable objects has special behavior:

async function awaitThenable() {
  const v = await {
    then(resolve) {
      setTimeout(() => resolve('Custom then'), 100);
    }
  };
  console.log(v); // 'Custom then' (after 100ms)
}

Special Behavior of Promise.allSettled

Unlike Promise.all, allSettled never rejects:

Promise.allSettled([
  Promise.resolve('Success'),
  Promise.reject('Failure')
]).then(results => {
  console.log(results);
  // [
  //   { status: "fulfilled", value: "Success" },
  //   { status: "rejected", reason: "Failure" }
  // ]
});

Interaction with Async Generator Functions

Async generator functions combine async and generator features:

async function* asyncGen() {
  yield await Promise.resolve(1);
  yield await Promise.resolve(2);
}

(async () => {
  for await (const num of asyncGen()) {
    console.log(num); // 1, then 2
  }
})();

This pattern is particularly useful for streaming data, but note that error propagation differs from regular async functions.

本站部分内容来自互联网,一切版权均归源网站或源作者所有。

如果侵犯了你的权益请来信告知我们删除。邮箱:cc@cccx.cn

Front End Chuan

Front End Chuan, Chen Chuan's Code Teahouse 🍵, specializing in exorcising all kinds of stubborn bugs 💻. Daily serving baldness-warning-level development insights 🛠️, with a bonus of one-liners that'll make you laugh for ten years 🐟. Occasionally drops pixel-perfect romance brewed in a coffee cup ☕.