빌드노트
Promise.all이 실패했는데 나머지 작업은 계속됐다
Promise.all의 catch는 남은 작업의 취소나 종료를 뜻하지 않는다. 짧은 Node 타이머에서 거절 뒤 완료와 명시 취소를 비교하고, allSettled로 결과를 관찰할지 취소를 요청할지 구분한다.
Promise.all을 await하다가 catch로 빠졌는데, 뒤늦게 다른 작업의 완료 로그가 찍힐 수 있다. 여러 결과를 모으는 Promise가 거절돼도 이미 시작한 다른 작업은 자동으로 취소되지 않는다. 남은 결과가 필요하면 같은 Promise들을 allSettled로 관찰한다. 남은 일이 불필요하다면 해당 작업 API가 지원하는 취소를 명시적으로 요청한다.
MDN의 Promise.all 설명 (새 탭에서 열림)은 입력 중 하나가 거절되면 반환한 Promise도 그 첫 거절 이유로 거절되며, 나머지 작업은 취소하지 않는다고 설명한다. 따라서 catch에 도착했다는 사실만으로 모든 작업이 끝났다고 표시하면 안 된다.
catch가 실행된 뒤에도 A와 C는 완료됐다
외부 요청 없이 타이머 세 개로 확인했다. A는 300ms, B는 100ms, C는 500ms를 기다리게 하고 B만 오류를 던진다. 아래 코드는 오류를 받은 시점과 나머지 완료 시점을 비교하기 위한 입력이다.
Node.js가 있는 빈 폴더에서 다음을 promise-observe.mjs로 저장하고, 같은 폴더에서 node promise-observe.mjs를 실행한다. 추가 패키지는 필요 없다. .mjs 확장자는 파일 최상위에서 await를 쓰기 위한 것이다. 실제 실행 환경은 Windows의 Node.js v22.23.2였다.
import { setTimeout as sleep } from 'node:timers/promises';
const started = Date.now();
const tasks = [
sleep(300).then(() => console.log('A complete', Date.now() - started)),
sleep(100).then(() => { throw new Error('B failed'); }),
sleep(500).then(() => console.log('C complete', Date.now() - started)),
];
try {
await Promise.all(tasks);
console.log('all fulfilled');
} catch (error) {
console.log('catch', error.message, Date.now() - started);
}
const results = await Promise.allSettled(tasks);
console.log('settled', Date.now() - started, results.map(r => r.status).join(' '));
실제 출력은 다음과 같다. 숫자는 Date.now()의 차이로 기록한 경과 밀리초다.
catch B failed 114
A complete 313
C complete 513
settled 514 fulfilled rejected fulfilled
배열을 만들면서 세 타이머를 모두 시작했다. B가 거절되자 all fulfilled는 실행되지 않았지만, A와 C는 그대로 기다린 뒤 완료됐다. 114ms의 실패 통지와 514ms의 전체 결과 확인은 서로 다른 시점이었다.
마지막 allSettled(tasks)는 같은 Promise들을 더 기다린다. 타이머를 다시 만드는 호출이 아니므로 A와 C를 재실행하지 않는다. MDN의 allSettled 설명 (새 탭에서 열림)에 따르면 결과는 모든 입력이 이행 또는 거절된 뒤 입력 순서대로 나온다. 위 세 상태는 A·B·C에 대응하며, 성공한 항목의 반환값은 value, 거절된 항목의 오류는 reason에서 읽는다.
정상 조건도 비교했다. B의 throw new Error('B failed');를 console.log('B complete', Date.now() - started);로 바꾸자 B → A → C 순서로 완료됐고, 그 뒤 all fulfilled와 세 개의 fulfilled가 나왔다. 배열의 순서와 완료 순서는 다를 수 있다.
남은 타이머를 멈추려면 신호를 연결한다
위 sleep은 Node의 Promise 기반 타이머다. Node.js v22.23.2 타이머 문서 (새 탭에서 열림)는 이 API에 AbortSignal을 전달해 취소할 수 있고, 취소된 타이머의 Promise는 AbortError로 거절된다고 설명한다.
B가 오류를 던지는 첫 코드를 promise-cancel.mjs로 복사한다. 취소 요청을 타이머에 전달할 수 있도록 const started = Date.now(); 다음에 두 변수를 추가한다.
const controller = new AbortController();
const options = { signal: controller.signal };
배열 안의 sleep(300)을 sleep(300, undefined, options)로 바꾼다. sleep(100)과 sleep(500)도 각각 sleep(100, undefined, options), sleep(500, undefined, options)로 바꾼다. 세 번째 인자가 옵션이므로, 두 번째 인자인 반환값 자리는 undefined로 둔다. 각 호출 뒤의 .then(...)은 그대로 둔다.
실패할 때 취소를 요청하도록 기존 catch 부분을 다음으로 교체한다.
} catch (error) {
console.log('catch', error.message, Date.now() - started);
controller.abort();
}
나머지 코드는 유지하고, 취소 이유도 확인하도록 파일 마지막에 console.log(results.map(r => r.reason?.name).join(' '));을 추가한다. 같은 폴더에서 node promise-cancel.mjs를 실행한 실제 출력이다.
catch B failed 113
settled 116 rejected rejected rejected
AbortError Error AbortError
A와 C의 complete는 나오지 않았다. 실패 처리 코드가 abort()를 호출했고, 아직 대기 중인 타이머가 그 신호를 처리했다. 최종 상태는 셋 다 rejected지만 이유는 다르다. A와 C는 취소에 따른 AbortError, B는 이미 발생한 원래 Error다.
같은 파일에서 const options = { signal: undefined };로만 바꿔 다시 실행하자, catch는 115ms에 나왔지만 A·C는 301/502ms에 완료됐다. 취소 기능이 있는 API여도 해당 호출에 신호를 전달하지 않으면 이 요청으로 멈추지 않는다. 이 대조는 신호를 전달하지 않은 경우를 확인한 것이며, 신호를 받아도 무시하는 제3자 라이브러리를 시험한 것은 아니다.
MDN의 AbortSignal 설명 (새 탭에서 열림)처럼 신호를 받아 처리하는 책임은 작업 API에 있다. 자기 코드에서는 작업 함수가 신호를 내부 API까지 전달하는지, 그 API가 취소를 지원하는지 확인한다. 취소를 지원하지 않는 작업은 Promise.all 주변에 abort()만 추가해도 멈추지 않는다.
실패 뒤에도 쓸 결과가 있는지로 결정한다
일부가 실패해도 다른 결과를 쓸 수 있다면 allSettled로 항목별 성공과 실패를 처리한다. 이 메서드의 이행이 모든 작업의 성공을 뜻하지는 않는다. 위 첫 실행에도 결과 안에 rejected가 남았다. 모든 입력의 종료를 기다리므로, 끝나지 않는 입력에 제한 시간을 붙여 주는 기능도 아니다.
남은 결과가 불필요하다면 실패 경로에서 취소를 요청하고, 같은 Promise들에 allSettled를 붙여 어떻게 끝났는지 확인할 수 있다. 다만 작업 함수가 내부 작업을 기다리지 않고 먼저 반환한다면, 그 Promise의 종료만으로 숨은 작업의 종료까지 증명할 수는 없다. 취소는 이미 발생한 부작용을 되돌리는 기능도 아니다.
이번에 직접 확인한 것은 짧은 로컬 타이머의 거절·완료와 취소 신호 처리다. 실제 브라우저 요청, 서버의 처리 중단, 데이터 쓰기와 롤백은 시험하지 않았다. 타이머 취소를 저장 취소나 롤백의 증거로 확대할 수 없다.
자기 환경에서 숫자가 달라도 먼저 catch, 각 작업의 완료, 최종 상태를 비교한다. Node 문서는 타이머의 정확한 실행 시각이나 순서를 보장하지 않는다. 취소를 넣었는데도 완료가 나온다면 신호 전달·API의 취소 지원·취소 전에 이미 완료했는지를 확인한다. node 명령을 찾지 못하거나 모듈·문법 오류가 나면 아직 이 동작을 비교한 것이 아니므로 Node 실행 환경과 .mjs 확장자부터 확인한다.