Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Select an option

  • Save carefree-ladka/22c565ae92197b8d2d56e0d81ccb20c5 to your computer and use it in GitHub Desktop.

Select an option

Save carefree-ladka/22c565ae92197b8d2d56e0d81ccb20c5 to your computer and use it in GitHub Desktop.
JavaScript Promises & Event Loop - Tricky Questions

JavaScript Promises & Event Loop - Tricky Questions

A comprehensive guide to understanding JavaScript promises, async/await, and the event loop through challenging quiz questions.


#1: Promise Executor Order

console.log('Start');

const promise = new Promise((resolve, reject) => {
  console.log('Promise executor');
  resolve('Resolved');
});

promise.then((value) => {
  console.log(value);
});

console.log('End');

Output:

Start
Promise executor
End
Resolved

Explanation: The promise executor runs synchronously when the promise is created. The .then() callback is scheduled as a microtask and runs after the current synchronous code completes.

Answer: Start, Promise executor, End, Resolved


#2: Promise Chain Return Values

Promise.resolve(1)
  .then((value) => {
    console.log(value);
    return value + 1;
  })
  .then((value) => {
    console.log(value);
  })
  .then((value) => {
    console.log(value);
    return Promise.resolve(3);
  })
  .then((value) => {
    console.log(value);
  });

Output:

1
2
undefined
3

Explanation: The second .then() doesn't return anything, so undefined is passed to the next .then(). The third .then() returns a promise that resolves to 3.

Answer: 1, 2, undefined, 3


#3: Promise.resolve Unwrapping

const promise1 = Promise.resolve(Promise.resolve(Promise.resolve(1)));

promise1.then((value) => {
  console.log(value);
});

Output:

1

Explanation: Promise.resolve() automatically unwraps nested promises. It recursively unwraps until it gets to a non-promise value.

Answer: 1


#4: Microtasks vs Macrotasks

console.log('1');

setTimeout(() => {
  console.log('2');
}, 0);

Promise.resolve().then(() => {
  console.log('3');
});

console.log('4');

Output:

1
4
3
2

Explanation: Synchronous code runs first (1, 4). Microtasks (promises) run before macrotasks (setTimeout), so 3 runs before 2.

Answer: 1, 4, 3, 2


#5: Error Handling in Chains

Promise.resolve('Start')
  .then((value) => {
    console.log(value);
    throw new Error('Oops!');
  })
  .then((value) => {
    console.log('Hello');
  })
  .catch((error) => {
    console.log('Caught:', error.message);
    return 'Recovered';
  })
  .then((value) => {
    console.log(value);
  });

Output:

Start
Caught: Oops!
Recovered

Explanation: The error skips the second .then() and goes directly to .catch(). The catch returns a resolved value, so the chain continues normally.

Answer: Start, Caught: Oops!, Recovered


#6: Promise Settles Once

const promise = new Promise((resolve, reject) => {
  resolve('First');
  resolve('Second');
  reject('Third');
});

promise
  .then((value) => console.log(value))
  .catch((error) => console.log(error));

Output:

First

Explanation: A promise can only settle once. After the first resolve('First'), all subsequent resolve/reject calls are ignored.

Answer: First


#7: Return Values and Rejections

new Promise((resolve, reject) => {
  resolve(1);
})
  .then((value) => {
    console.log(value);
    return 2;
  })
  .then((value) => {
    console.log(value);
    // No return statement
  })
  .then((value) => {
    console.log(value);
    return Promise.reject('Error');
  })
  .catch((error) => {
    console.log(error);
  })
  .then(() => {
    console.log('Done');
  });

Output:

1
2
undefined
Error
Done

Explanation: Missing return statements result in undefined. After .catch(), the chain continues with a resolved promise.

Answer: 1, 2, undefined, Error, Done


#8: Async/Await and Microtasks

async function test() {
  console.log('1');
  await Promise.resolve();
  console.log('2');
}

console.log('3');
test();
console.log('4');

Output:

3
1
4
2

Explanation: test() runs synchronously until await, which schedules the rest as a microtask. Synchronous code (4) completes first, then microtask (2) runs.

Answer: 3, 1, 4, 2


#9: Promise.all Fail Fast

const promise1 = Promise.resolve(1);
const promise2 = Promise.reject('Error');
const promise3 = Promise.resolve(3);

Promise.all([promise1, promise2, promise3])
  .then((values) => {
    console.log('Success:', values);
  })
  .catch((error) => {
    console.log('Failed:', error);
  });

Output:

Failed: Error

Explanation: Promise.all() rejects immediately when any promise rejects. It doesn't wait for other promises to settle.

Answer: Failed: Error


#10: Nested Promise Chains

Promise.resolve(1)
  .then((value) => {
    console.log(value);
    Promise.resolve(2)
      .then((value) => {
        console.log(value);
      });
    return 3;
  })
  .then((value) => {
    console.log(value);
  });

Output:

1
3
2

Explanation: The nested promise creates a separate microtask queue entry. The outer chain continues immediately with the return value (3), then the nested promise resolves (2).

Answer: 1, 3, 2


#11: Promise.race First Settler

const promise1 = new Promise((resolve) => {
  setTimeout(() => resolve('Slow'), 1000);
});

const promise2 = new Promise((resolve) => {
  setTimeout(() => resolve('Fast'), 100);
});

const promise3 = new Promise((resolve, reject) => {
  setTimeout(() => reject('Error'), 50);
});

Promise.race([promise1, promise2, promise3])
  .then((value) => {
    console.log('Winner:', value);
  })
  .catch((error) => {
    console.log('Failed:', error);
  });

Output:

Failed: Error

Explanation: Promise.race() settles with the first promise that settles, whether resolved or rejected. The error at 50ms wins.

Answer: Failed: Error


#12: Multiple Catches

Promise.reject('First error')
  .catch((error) => {
    console.log('Catch 1:', error);
    throw new Error('Second error');
  })
  .catch((error) => {
    console.log('Catch 2:', error.message);
    return 'Recovered';
  })
  .then((value) => {
    console.log('Then:', value);
    throw new Error('Third error');
  })
  .catch((error) => {
    console.log('Catch 3:', error.message);
  });

Output:

Catch 1: First error
Catch 2: Second error
Then: Recovered
Catch 3: Third error

Explanation: Each .catch() can handle errors and either recover (return) or propagate new errors (throw). The chain continues based on what each handler does.

Answer: Catch 1: First error, Catch 2: Second error, Then: Recovered, Catch 3: Third error


#13: Async Function Return Values

async function func1() {
  return 1;
}

async function func2() {
  return Promise.resolve(2);
}

func1().then(console.log);
func2().then(console.log);
console.log(3);

Output:

3
1
2

Explanation: Async functions always return promises. Both .then() callbacks are microtasks that run after synchronous code (3). They execute in order: 1, then 2.

Answer: 3, 1, 2


#14: Promise.finally Behavior

Promise.resolve('Success')
  .finally(() => {
    console.log('Finally 1');
    return 'This will be ignored';
  })
  .then((value) => {
    console.log('Then 1:', value);
  })
  .finally(() => {
    console.log('Finally 2');
    throw new Error('Finally error');
  })
  .then((value) => {
    console.log('Then 2:', value);
  })
  .catch((error) => {
    console.log('Catch:', error.message);
  });

Output:

Finally 1
Then 1: Success
Finally 2
Catch: Finally error

Explanation: .finally() doesn't change the promise value unless it throws an error or returns a rejected promise. Return values are ignored.

Answer: Finally 1, Then 1: Success, Finally 2, Catch: Finally error


#15: Complex Event Loop Ordering

console.log('Start');

setTimeout(() => {
  console.log('Timeout 1');
  Promise.resolve().then(() => console.log('Promise in Timeout'));
}, 0);

Promise.resolve()
  .then(() => {
    console.log('Promise 1');
    setTimeout(() => console.log('Timeout in Promise'), 0);
  })
  .then(() => {
    console.log('Promise 2');
  });

setTimeout(() => {
  console.log('Timeout 2');
}, 0);

console.log('End');

Output:

Start
End
Promise 1
Promise 2
Timeout 1
Promise in Timeout
Timeout 2
Timeout in Promise

Explanation:

  1. Synchronous: Start, End
  2. Microtasks: Promise 1, Promise 2
  3. Macrotask (Timeout 1) runs, creates microtask (Promise in Timeout)
  4. Microtask runs: Promise in Timeout
  5. Macrotask: Timeout 2
  6. Macrotask: Timeout in Promise

Answer: Start, End, Promise 1, Promise 2, Timeout 1, Promise in Timeout, Timeout 2, Timeout in Promise


#16: Executor Sync, Resolve Async

const promise = new Promise((resolve, reject) => {
  console.log('Executor start');
  setTimeout(() => {
    console.log('Timeout in executor');
    resolve('Done');
  }, 0);
  console.log('Executor end');
});

promise.then((value) => {
  console.log('Then:', value);
});

console.log('After promise creation');

Output:

Executor start
Executor end
After promise creation
Timeout in executor
Then: Done

Explanation: The executor runs synchronously. setTimeout schedules a macrotask. Synchronous code completes first, then the timeout runs and resolves the promise.

Answer: Executor start, Executor end, After promise creation, Timeout in executor, Then: Done


#17: Complex Microtask and Macrotask Mix

console.log(1);

setTimeout(() => {
  console.log(2);
}, 10);

setTimeout(() => {
  console.log(3);
}, 0);

new Promise((_, reject) => {
  console.log(4);
  reject(5);
  console.log(6);
})
  .then(() => console.log(7))
  .catch(() => console.log(8))
  .then(() => console.log(9))
  .catch(() => console.log(10))
  .then(() => console.log(11))
  .then(console.log)
  .finally(() => console.log(12));

console.log(13);

Output:

1
4
6
13
8
9
11
undefined
12
3
2

Explanation:

  1. Synchronous: 1, 4, 6, 13
  2. Microtasks (promise chain):
    • Rejection caught: 8
    • Chain continues: 9, 11
    • .then(console.log) logs undefined (no return value)
    • Finally: 12
  3. Macrotasks: 3 (0ms timeout), then 2 (10ms timeout)

Key Points:

  • Promise executor runs synchronously
  • Reject doesn't stop executor (6 still logs)
  • .catch() recovers the chain
  • .then(console.log) without return logs undefined
  • Microtasks drain completely before macrotasks
  • Timeouts execute in order of delay

Event Loop Fundamentals

Execution Order:

  1. Synchronous code - Runs immediately
  2. Microtasks - Promises, queueMicrotask
  3. Macrotasks - setTimeout, setInterval, I/O

Key Concepts:

Promise Executor:

  • Runs synchronously when promise is created
  • Cannot be cancelled once started

Microtask Queue:

  • .then(), .catch(), .finally() callbacks
  • Drains completely before next macrotask
  • New microtasks added during processing still run in same cycle

Macrotask Queue:

  • setTimeout, setInterval callbacks
  • One macrotask per event loop cycle
  • After each macrotask, microtask queue drains

Async/Await:

  • Code before await runs synchronously
  • Code after await becomes a microtask
  • Equivalent to .then() callback

Promise Settling:

  • Promises settle only once
  • Subsequent resolve/reject calls ignored
  • Settlement is irreversible

Practice Tips

  1. Track execution order: Synchronous → Microtasks → Macrotasks
  2. Watch for nested promises: They create separate microtask entries
  3. Return values matter: Missing returns pass undefined
  4. Errors skip .then(): Go directly to next .catch()
  5. Finally is special: Doesn't change promise value (unless it throws)
  6. Async always returns promise: Even plain values get wrapped

Additional Tricky Questions - Nested Patterns

#18: Promise Inside setTimeout

console.log(1);

setTimeout(() => {
  console.log(3);
  Promise.resolve().then(() => console.log(4));
}, 0);

Promise.resolve().then(() => {
  console.log(5);
  setTimeout(() => {
    console.log(7);
  }, 0);
});

console.log(6);

Output:

1
6
5
3
4
7

Explanation:

  1. Synchronous: 1, 6
  2. Microtask: 5 (creates setTimeout with 7)
  3. Macrotask 1: 3 (creates microtask with 4)
  4. Microtask: 4 (from inside first setTimeout)
  5. Macrotask 2: 7 (from inside first promise)

Key Point: When a macrotask creates a microtask, that microtask runs before the next macrotask!


#19: Multiple Nested Levels

console.log('A');

setTimeout(() => {
  console.log('B');
  Promise.resolve().then(() => {
    console.log('C');
    setTimeout(() => console.log('D'), 0);
  });
}, 0);

Promise.resolve().then(() => {
  console.log('E');
  setTimeout(() => {
    console.log('F');
    Promise.resolve().then(() => console.log('G'));
  }, 0);
});

setTimeout(() => console.log('H'), 0);

console.log('I');

Output:

A
I
E
B
C
H
F
G
D

Explanation:

  1. Sync: A, I
  2. Microtask: E (schedules F-G timeout)
  3. Macrotask: B (schedules C microtask)
  4. Microtask: C (schedules D timeout)
  5. Macrotask: H
  6. Macrotask: F (schedules G microtask)
  7. Microtask: G
  8. Macrotask: D

#20: setTimeout Zero with Promises

console.log(1);

setTimeout(() => {
  console.log(2);
  Promise.resolve()
    .then(() => console.log(3))
    .then(() => console.log(4));
}, 0);

setTimeout(() => {
  console.log(5);
  Promise.resolve()
    .then(() => console.log(6));
}, 0);

Promise.resolve()
  .then(() => {
    console.log(7);
    setTimeout(() => console.log(8), 0);
  })
  .then(() => console.log(9));

console.log(10);

Output:

1
10
7
9
2
3
4
5
6
8

Explanation:

  1. Sync: 1, 10
  2. Microtasks: 7 (schedules 8), then 9
  3. Macrotask: 2 (schedules 3, 4 microtasks)
  4. Microtasks: 3, 4 (drain before next macrotask)
  5. Macrotask: 5 (schedules 6 microtask)
  6. Microtask: 6
  7. Macrotask: 8

#21: Deep Nesting Pattern

setTimeout(() => {
  console.log('Timeout 1');
  Promise.resolve()
    .then(() => {
      console.log('Promise 1');
      setTimeout(() => {
        console.log('Timeout 2');
        Promise.resolve().then(() => console.log('Promise 2'));
      }, 0);
    })
    .then(() => console.log('Promise 3'));
}, 0);

console.log('Start');

Output:

Start
Timeout 1
Promise 1
Promise 3
Timeout 2
Promise 2

Explanation: Microtasks created within a macrotask must complete before the next macrotask runs. This creates a cascading effect where each level fully resolves its microtasks.


#22: Promise Constructor Pitfall

const p = new Promise((resolve) => {
  return resolve(Promise.resolve(2));
});

p.then(console.log);

Output: 2

Explanation: Promise constructors unwrap returned promises just like Promise.resolve().


#23: Throw in Finally

Promise.resolve(1)
  .finally(() => {
    throw new Error('Finally error');
  })
  .then(
    (value) => console.log('Success:', value),
    (error) => console.log('Error:', error.message)
  );

Output: Error: Finally error

Explanation: Throwing in .finally() rejects the promise, overriding the original value.


#24: Async Function Exception

async function test() {
  throw new Error('Async error');
}

test().catch((error) => console.log('Caught:', error.message));
console.log('After test call');

Output:

After test call
Caught: Async error

Explanation: Async function exceptions are caught asynchronously as microtasks, so synchronous code runs first.


#25: Interleaved Nesting

console.log('Start');

Promise.resolve().then(() => {
  console.log('P1');
  setTimeout(() => console.log('T1'), 0);
});

setTimeout(() => {
  console.log('T2');
  Promise.resolve().then(() => console.log('P2'));
}, 0);

Promise.resolve().then(() => {
  console.log('P3');
  setTimeout(() => console.log('T3'), 0);
});

setTimeout(() => {
  console.log('T4');
  Promise.resolve().then(() => console.log('P4'));
}, 0);

console.log('End');

Output:

Start
End
P1
P3
T2
P2
T4
P4
T1
T3

Explanation:

  1. Sync: Start, End
  2. Microtasks: P1 (schedules T1), P3 (schedules T3)
  3. Macrotask: T2 (schedules P2 microtask)
  4. Microtask: P2 (drains before next macrotask)
  5. Macrotask: T4 (schedules P4 microtask)
  6. Microtask: P4
  7. Macrotasks: T1, T3 (scheduled from earlier promises)

#26: Triple Nesting

console.log(1);

setTimeout(() => {
  console.log(2);
  Promise.resolve().then(() => {
    console.log(3);
    setTimeout(() => {
      console.log(4);
      Promise.resolve().then(() => console.log(5));
    }, 0);
  });
  setTimeout(() => console.log(6), 0);
}, 0);

Promise.resolve().then(() => {
  console.log(7);
  setTimeout(() => {
    console.log(8);
    Promise.resolve().then(() => console.log(9));
  }, 0);
});

console.log(10);

Output:

1
10
7
2
3
6
8
9
4
5

Explanation: This demonstrates the "microtask barrier" - microtasks always drain completely before moving to the next macrotask, creating predictable ordering even with deep nesting.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment