A comprehensive guide to understanding JavaScript promises, async/await, and the event loop through challenging quiz questions.
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 ✓
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 ✓
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 ✓
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 ✓
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 ✓
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 ✓
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 ✓
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 ✓
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 ✓
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 ✓
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 ✓
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 ✓
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 ✓
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 ✓
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:
- Synchronous: Start, End
- Microtasks: Promise 1, Promise 2
- Macrotask (Timeout 1) runs, creates microtask (Promise in Timeout)
- Microtask runs: Promise in Timeout
- Macrotask: Timeout 2
- Macrotask: Timeout in Promise
Answer: Start, End, Promise 1, Promise 2, Timeout 1, Promise in Timeout, Timeout 2, Timeout in Promise ✓
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 ✓
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:
- Synchronous: 1, 4, 6, 13
- Microtasks (promise chain):
- Rejection caught: 8
- Chain continues: 9, 11
.then(console.log)logsundefined(no return value)- Finally: 12
- 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 logsundefined- Microtasks drain completely before macrotasks
- Timeouts execute in order of delay
- Synchronous code - Runs immediately
- Microtasks - Promises, queueMicrotask
- Macrotasks - setTimeout, setInterval, I/O
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
awaitruns synchronously - Code after
awaitbecomes a microtask - Equivalent to
.then()callback
Promise Settling:
- Promises settle only once
- Subsequent resolve/reject calls ignored
- Settlement is irreversible
- Track execution order: Synchronous → Microtasks → Macrotasks
- Watch for nested promises: They create separate microtask entries
- Return values matter: Missing returns pass
undefined - Errors skip .then(): Go directly to next
.catch() - Finally is special: Doesn't change promise value (unless it throws)
- Async always returns promise: Even plain values get wrapped
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:
- Synchronous: 1, 6
- Microtask: 5 (creates setTimeout with 7)
- Macrotask 1: 3 (creates microtask with 4)
- Microtask: 4 (from inside first setTimeout)
- Macrotask 2: 7 (from inside first promise)
Key Point: When a macrotask creates a microtask, that microtask runs before the next macrotask!
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:
- Sync: A, I
- Microtask: E (schedules F-G timeout)
- Macrotask: B (schedules C microtask)
- Microtask: C (schedules D timeout)
- Macrotask: H
- Macrotask: F (schedules G microtask)
- Microtask: G
- Macrotask: D
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:
- Sync: 1, 10
- Microtasks: 7 (schedules 8), then 9
- Macrotask: 2 (schedules 3, 4 microtasks)
- Microtasks: 3, 4 (drain before next macrotask)
- Macrotask: 5 (schedules 6 microtask)
- Microtask: 6
- Macrotask: 8
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.
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().
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.
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.
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:
- Sync: Start, End
- Microtasks: P1 (schedules T1), P3 (schedules T3)
- Macrotask: T2 (schedules P2 microtask)
- Microtask: P2 (drains before next macrotask)
- Macrotask: T4 (schedules P4 microtask)
- Microtask: P4
- Macrotasks: T1, T3 (scheduled from earlier promises)
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.