Promise.all과 Short-Circuit, Timeout
수정일: 2025. 4. 30.
숏서킷을 알아야하는 이유
Promise.all은 Short-Circuit으로 작동합니다
Short-Circuit
Short-Circuit
은 합선이라는 의미를 가지고 있습니다.
개발에선 하나라도 실패하면 다른 작업을 기다리지 않고 즉시 종료시키는 것을 의미합니다.
1 && false && 2 // false => 2가 반환되지 않은 이유는 false에 의해 Short-Circuit되었기 때문
Promise.all과 Short-Circuit
자바스크립트의 Promise.all
은 이 Short-Circuit
방식을 따릅니다.
Promise.method
들은 아래와 같이 표로 정리할 수 있습니다:
이름 | 방식 | 비고 |
---|---|---|
Promise.allSettled | Short-Circuit이 아님 | 모든 Promise의 결과를 기다림, Short-Circuit이 없음 |
Promise.all | reject에 의한 Short-Circuit | 하나라도 rejected되면 Short-Circuit |
Promise.race | resolve에 의한 Short-Circuit | 가장 먼저 fulfilled된 값 반환 |
Promise.any | fulfilled에 의한 Short-Circuit | 하나라도 fulfilled이면 즉시 반환, 모두 실패 시 AggregateError |
개별 Promise의 Timeout을 설정하지 않으면 전체 요청이 오래 걸릴 수 있다
어떤 API 요청들이 서로 의존하지 않는 경우, Promise.all
을 통해 병렬로 네트워크 요청을 시작 할 수 있습니다.
- 네트워크 요청을 보낼 때 개별 요청마다 Timeout을 설정하는 것이 중요합니다.
Promise.all
로 묶여 있는 경우, 하나의 요청이 지연되면 전체 응답이 늦어질 수 있습니다.
const withTimeout = (promise, time) => {
return Promise.race([
promise,
new Promise((_, rej) =>
setTimeout(() => rej(new Error(`${time}ms TIMEOUT`)), time)
),
]);
};
const wait = (ms) => new Promise((res) => setTimeout(() => res(`wait 완료`), ms));
async function main() {
console.log("Start");
try {
await Promise.all([
withTimeout(wait(3000), 1500), // 1.5초 내에 완료되지 않으면 Timeout
withTimeout(wait(2000), 2500), // 정상 완료됨
]);
} catch (error) {
console.log("🚀 ~ main ~ error:", error);
}
}
main();
위 코드는 Short-Circuit
으로 동작합니다.
여러 Promise들 중 하나라도 reject되면 Promise.all
은 catch
로 흐름이 이동합니다.
하지만 Promise.all
로 묶여있는 모든 Promise가 reject된다는 뜻은 아닙니다.
→ 각 Promise는 개별적으로 resolve/reject되며, Promise.all
은 가장 먼저 reject된 것을 기준으로 종료됩니다.
이 말은
Promise.all
이 종료된다고 해서 내부의 개별 Promise가 중단되는 것은 아니라는 것입니다.
성공한 Promise는 자신의resolve
콜백을 실행하지만, 결과는 무시됩니다.
이는DB의 transaction
처럼 완전히 rollback되는 구조는 아니라는 점에서 중요합니다.
또한 개별 Timeout이 동일한 경우, 어떤 Promise가 reject되는지는 HTTP 요청 선착순에 따라 결정되며,
미리 알 수 없습니다.
// 아래의 두 Promise 중 어떤 게 reject될지 모름
await Promise.all([
withTimeout(wait(3000), 2500),
withTimeout(wait(3000), 2500),
]);
이름을 명시해서 log에 남기기
제가 생각하는 대응은
각 Promise가 reject될 때 추적하기 용이하도록 Error handling이 되어 있어야 한다는 점입니다.
요청 URL이나 식별 가능한 이름을 추가하면 로그 추적이 수월해집니다.
const timeout = (time) => {
return new Promise((_, rej) =>
setTimeout(
() => rej(new Error(`[timeout function]: ${time}ms, TIME OUT ERROR`)),
time
)
);
};
API별로 catch하고 기본 값을 반환하기
Promise.all
이 Short-Circuit
을 일으키더라도,
각 Promise
별로 catch
를 해주면 Short-Circuit
을 막을 수 있습니다.
Short-Circuit
을 막는 것이 좋은 패턴은 아닐 수 있습니다.
중요한 것은 reject에 대한 핸들링입니다.
이렇게 할 바엔 차라리 Promise.allSettled
를 쓰는 것이 낫습니다:
await Promise.all([
withTimeout(wait(3000), 2500).catch(e => undefined),
withTimeout(wait(3000), 2500).catch(e => undefined),
]);
Promise.allSettled를 사용하여 대응하기
Short-Circuit
없이 모든 Promise를 기다리려면 Promise.allSettled
를 사용하면 됩니다.
async function main() {
console.log("test start:");
const results = await Promise.allSettled([
wait(1000),
timeout(2000),
]);
console.log("🚀 ~ main ~ results:", results);
}
main();
예시 결과:
🚀 ~ main ~ results: [
{ status: 'fulfilled', value: 'wait 완료' },
{ status: 'rejected', reason: Error: 2000ms, TIME OUT ERROR }
]
요약
Short-Circuit
을 회피해야 하는 것은 아니지만,
회피가 필요한 경우 적절히 제어할 수 있어야 합니다.
Promise.all
내 Promise에서 reject 발생 시, catch가 없다면 어떤 것이 실패했는지 추적이 어렵습니다.Promise
를 구분할 수 있는 로그 메시지를 남기고,
필요한 경우Promise.allSettled
를 사용하는 것이 좋습니다.