Skip to main content

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.