├── .github └── workflows │ ├── ci.yml │ ├── commit-if-modified.sh │ ├── copyright-year.sh │ ├── isaacs-makework.yml │ └── package-json-repo.js ├── .gitignore ├── LICENSE ├── README.md ├── index.js ├── package-lock.json ├── package.json └── test └── index.js /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | build: 7 | strategy: 8 | matrix: 9 | node-version: [12.x, 14.x, 16.x, 17.x] 10 | platform: 11 | - os: ubuntu-latest 12 | shell: bash 13 | - os: macos-latest 14 | shell: bash 15 | - os: windows-latest 16 | shell: bash 17 | - os: windows-latest 18 | shell: powershell 19 | fail-fast: false 20 | 21 | runs-on: ${{ matrix.platform.os }} 22 | defaults: 23 | run: 24 | shell: ${{ matrix.platform.shell }} 25 | 26 | steps: 27 | - name: Checkout Repository 28 | uses: actions/checkout@v1.1.0 29 | 30 | - name: Use Nodejs ${{ matrix.node-version }} 31 | uses: actions/setup-node@v1 32 | with: 33 | node-version: ${{ matrix.node-version }} 34 | 35 | - name: Install dependencies 36 | run: npm install 37 | 38 | - name: Run Tests 39 | run: npm test -- -c -t0 40 | -------------------------------------------------------------------------------- /.github/workflows/commit-if-modified.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | git config --global user.email "$1" 3 | shift 4 | git config --global user.name "$1" 5 | shift 6 | message="$1" 7 | shift 8 | if [ $(git status --porcelain "$@" | egrep '^ M' | wc -l) -gt 0 ]; then 9 | git add "$@" 10 | git commit -m "$message" 11 | git push || git pull --rebase 12 | git push 13 | fi 14 | -------------------------------------------------------------------------------- /.github/workflows/copyright-year.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | dir=${1:-$PWD} 3 | dates=($(git log --date=format:%Y --pretty=format:'%ad' --reverse | sort | uniq)) 4 | if [ "${#dates[@]}" -eq 1 ]; then 5 | datestr="${dates}" 6 | else 7 | datestr="${dates}-${dates[${#dates[@]}-1]}" 8 | fi 9 | 10 | stripDate='s/^((.*)Copyright\b(.*?))((?:,\s*)?(([0-9]{4}\s*-\s*[0-9]{4})|(([0-9]{4},\s*)*[0-9]{4})))(?:,)?\s*(.*)\n$/$1$9\n/g' 11 | addDate='s/^.*Copyright(?:\s*\(c\))? /Copyright \(c\) '$datestr' /g' 12 | for l in $dir/LICENSE*; do 13 | perl -pi -e "$stripDate" $l 14 | perl -pi -e "$addDate" $l 15 | done 16 | -------------------------------------------------------------------------------- /.github/workflows/isaacs-makework.yml: -------------------------------------------------------------------------------- 1 | name: "various tidying up tasks to silence nagging" 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | workflow_dispatch: 8 | 9 | jobs: 10 | makework: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - uses: actions/checkout@v2 14 | with: 15 | fetch-depth: 0 16 | - name: Use Node.js 17 | uses: actions/setup-node@v2.1.4 18 | with: 19 | node-version: 16.x 20 | - name: put repo in package.json 21 | run: node .github/workflows/package-json-repo.js 22 | - name: check in package.json if modified 23 | run: | 24 | bash -x .github/workflows/commit-if-modified.sh \ 25 | "package-json-repo-bot@example.com" \ 26 | "package.json Repo Bot" \ 27 | "chore: add repo to package.json" \ 28 | package.json package-lock.json 29 | - name: put all dates in license copyright line 30 | run: bash .github/workflows/copyright-year.sh 31 | - name: check in licenses if modified 32 | run: | 33 | bash .github/workflows/commit-if-modified.sh \ 34 | "license-year-bot@example.com" \ 35 | "License Year Bot" \ 36 | "chore: add copyright year to license" \ 37 | LICENSE* 38 | -------------------------------------------------------------------------------- /.github/workflows/package-json-repo.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | const pf = require.resolve(`${process.cwd()}/package.json`) 4 | const pj = require(pf) 5 | 6 | if (!pj.repository && process.env.GITHUB_REPOSITORY) { 7 | const fs = require('fs') 8 | const server = process.env.GITHUB_SERVER_URL || 'https://github.com' 9 | const repo = `${server}/${process.env.GITHUB_REPOSITORY}` 10 | pj.repository = repo 11 | const json = fs.readFileSync(pf, 'utf8') 12 | const match = json.match(/^\s*\{[\r\n]+([ \t]*)"/) 13 | const indent = match[1] 14 | const output = JSON.stringify(pj, null, indent || 2) + '\n' 15 | fs.writeFileSync(pf, output) 16 | } 17 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # ignore most things, include some others 2 | /* 3 | /.* 4 | 5 | !bin/ 6 | !lib/ 7 | !docs/ 8 | !package.json 9 | !package-lock.json 10 | !README.md 11 | !CONTRIBUTING.md 12 | !LICENSE 13 | !CHANGELOG.md 14 | !example/ 15 | !scripts/ 16 | !tap-snapshots/ 17 | !test/ 18 | !.github/ 19 | !.travis.yml 20 | !.gitignore 21 | !.gitattributes 22 | !coverage-map.js 23 | !map.js 24 | !index.js 25 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The ISC License 2 | 3 | Copyright (c) 2019-2023 Isaac Z. Schlueter 4 | 5 | Permission to use, copy, modify, and/or distribute this software for any 6 | purpose with or without fee is hereby granted, provided that the above 7 | copyright notice and this permission notice appear in all copies. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 10 | WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 11 | MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 12 | ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 13 | WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 14 | ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR 15 | IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 16 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # promise-all-reject-late 2 | 3 | Like Promise.all, but save rejections until all promises are resolved. 4 | 5 | This is handy when you want to do a bunch of things in parallel, and 6 | rollback on failure, without clobbering or conflicting with those parallel 7 | actions that may be in flight. For example, creating a bunch of files, 8 | and deleting any if they don't all succeed. 9 | 10 | Example: 11 | 12 | ```js 13 | const lateReject = require('promise-all-reject-late') 14 | 15 | const { promisify } = require('util') 16 | const fs = require('fs') 17 | const writeFile = promisify(fs.writeFile) 18 | 19 | const createFilesOrRollback = (files) => { 20 | return lateReject(files.map(file => writeFile(file, 'some data'))) 21 | .catch(er => { 22 | // try to clean up, then fail with the initial error 23 | // we know that all write attempts are finished at this point 24 | return lateReject(files.map(file => rimraf(file))) 25 | .catch(er => { 26 | console.error('failed to clean up, youre on your own i guess', er) 27 | }) 28 | .then(() => { 29 | // fail with the original error 30 | throw er 31 | }) 32 | }) 33 | } 34 | ``` 35 | 36 | ## API 37 | 38 | * `lateReject([array, of, promises])` - Resolve all the promises, 39 | returning a promise that rejects with the first error, or resolves with 40 | the array of results, but only after all promises are settled. 41 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | const allSettled = 2 | Promise.allSettled ? promises => Promise.allSettled(promises) 3 | : promises => { 4 | const reflections = [] 5 | for (let i = 0; i < promises.length; i++) { 6 | reflections[i] = Promise.resolve(promises[i]).then(value => ({ 7 | status: 'fulfilled', 8 | value, 9 | }), reason => ({ 10 | status: 'rejected', 11 | reason, 12 | })) 13 | } 14 | return Promise.all(reflections) 15 | } 16 | 17 | module.exports = promises => allSettled(promises).then(results => { 18 | let er = null 19 | const ret = new Array(results.length) 20 | results.forEach((result, i) => { 21 | if (result.status === 'rejected') 22 | throw result.reason 23 | else 24 | ret[i] = result.value 25 | }) 26 | return ret 27 | }) 28 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "promise-all-reject-late", 3 | "version": "1.0.1", 4 | "description": "Like Promise.all, but save rejections until all promises are resolved", 5 | "author": "Isaac Z. Schlueter (https://izs.me)", 6 | "license": "ISC", 7 | "scripts": { 8 | "test": "tap", 9 | "preversion": "npm test", 10 | "postversion": "npm publish", 11 | "prepublishOnly": "git push origin --follow-tags" 12 | }, 13 | "tap": { 14 | "check-coverage": true 15 | }, 16 | "devDependencies": { 17 | "tap": "^15.1.6" 18 | }, 19 | "funding": { 20 | "url": "https://github.com/sponsors/isaacs" 21 | }, 22 | "repository": "https://github.com/isaacs/promise-all-reject-late" 23 | } 24 | -------------------------------------------------------------------------------- /test/index.js: -------------------------------------------------------------------------------- 1 | const t = require('tap') 2 | 3 | const main = () => { 4 | if (process.argv[2] === 'polyfill-all-settled') { 5 | Promise.allSettled = null 6 | runTests() 7 | } else if (process.argv[2] === 'native-all-settled') { 8 | Promise.allSettled = Promise.allSettled || ( 9 | promises => { 10 | const reflections = [] 11 | for (let i = 0; i < promises.length; i++) { 12 | reflections[i] = Promise.resolve(promises[i]).then(value => ({ 13 | status: 'fulfilled', 14 | value, 15 | }), reason => ({ 16 | status: 'rejected', 17 | reason, 18 | })) 19 | } 20 | return Promise.all(reflections) 21 | } 22 | ) 23 | runTests() 24 | } else { 25 | t.spawn(process.execPath, [__filename, 'polyfill-all-settled']) 26 | t.spawn(process.execPath, [__filename, 'native-all-settled']) 27 | } 28 | } 29 | 30 | const runTests = () => { 31 | const lateFail = require('../') 32 | 33 | t.test('fail only after all promises resolve', t => { 34 | let resolvedSlow = false 35 | const fast = () => Promise.reject('nope') 36 | const slow = () => new Promise(res => setTimeout(res, 100)) 37 | .then(() => resolvedSlow = true) 38 | 39 | // throw some holes and junk in the array to verify that we handle it 40 | return t.rejects(lateFail([fast(),,,,slow(), null, {not: 'a promise'},,,])) 41 | .then(() => t.equal(resolvedSlow, true, 'resolved slow before failure')) 42 | }) 43 | 44 | t.test('works just like Promise.all() otherwise', t => { 45 | const one = () => Promise.resolve(1) 46 | const two = () => Promise.resolve(2) 47 | const tre = () => Promise.resolve(3) 48 | const fur = () => Promise.resolve(4) 49 | const fiv = () => Promise.resolve(5) 50 | const six = () => Promise.resolve(6) 51 | const svn = () => Promise.resolve(7) 52 | const eit = () => Promise.resolve(8) 53 | const nin = () => Promise.resolve(9) 54 | const ten = () => Promise.resolve(10) 55 | const expect = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] 56 | const all = Promise.all([ 57 | one(), 58 | two(), 59 | tre(), 60 | fur(), 61 | fiv(), 62 | six(), 63 | svn(), 64 | eit(), 65 | nin(), 66 | ten(), 67 | ]) 68 | const late = lateFail([ 69 | one(), 70 | two(), 71 | tre(), 72 | fur(), 73 | fiv(), 74 | six(), 75 | svn(), 76 | eit(), 77 | nin(), 78 | ten(), 79 | ]) 80 | 81 | return Promise.all([all, late]).then(([all, late]) => { 82 | t.strictSame(all, expect) 83 | t.strictSame(late, expect) 84 | }) 85 | }) 86 | } 87 | 88 | main() 89 | --------------------------------------------------------------------------------