├── .github └── workflows │ ├── ci.yml │ ├── commit-if-modified.sh │ ├── copyright-year.sh │ ├── isaacs-makework.yml │ └── package-json-repo.js ├── .gitignore ├── CHANGELOG.md ├── LICENSE ├── README.md ├── package-lock.json ├── package.json ├── scripts └── fixup.sh ├── src └── index.ts ├── tap-snapshots └── test │ ├── run.js-test-fixtures-basic.js.test.cjs │ ├── run.js-test-fixtures-child.js.test.cjs │ ├── run.js-test-fixtures-debug.js.test.cjs │ ├── run.js-test-fixtures-destroy.js.test.cjs │ ├── run.js-test-fixtures-multiple-destroy.js.test.cjs │ ├── run.js-test-fixtures-no-handler.js.test.cjs │ ├── run.js-test-fixtures-other-handler.js.test.cjs │ ├── run.js-test-fixtures-process-clubbing.js.test.cjs │ ├── run.js-test-fixtures-process-missing.js.test.cjs │ ├── run.js-test-fixtures-promise-no-domain.js.test.cjs │ ├── run.js-test-fixtures-promise-rejections-unhandled-none.js.test.cjs │ ├── run.js-test-fixtures-promise-rejections-unhandled-strict-with-space.js.test.cjs │ ├── run.js-test-fixtures-promise-rejections-unhandled-strict.js.test.cjs │ ├── run.js-test-fixtures-promise-rejections-unhandled-throw.js.test.cjs │ ├── run.js-test-fixtures-promise-rejections-unhandled-warn-with-error-code.js.test.cjs │ ├── run.js-test-fixtures-promise-rejections-unhandled-warn.js.test.cjs │ ├── run.js-test-fixtures-promise-rejections.js.test.cjs │ ├── run.js-test-fixtures-promise-throwing-handler.js.test.cjs │ ├── run.js-test-fixtures-promise-timing.js.test.cjs │ ├── run.js-test-fixtures-promise.js.test.cjs │ ├── run.js-test-fixtures-throw-in-handler.js.test.cjs │ └── run.js-test-fixtures-uncaught.js.test.cjs ├── test ├── fixtures │ ├── basic.js │ ├── child.js │ ├── debug.js │ ├── destroy.js │ ├── multiple-destroy.js │ ├── no-handler.js │ ├── other-handler.js │ ├── process-clubbing.js │ ├── process-missing.js │ ├── promise-no-domain.js │ ├── promise-rejections-unhandled-none.js │ ├── promise-rejections-unhandled-strict-with-space.js │ ├── promise-rejections-unhandled-strict.js │ ├── promise-rejections-unhandled-throw.js │ ├── promise-rejections-unhandled-warn-with-error-code.js │ ├── promise-rejections-unhandled-warn.js │ ├── promise-rejections.js │ ├── promise-throwing-handler.js │ ├── promise-timing.js │ ├── promise.js │ ├── throw-in-handler.js │ └── uncaught.js └── run.js ├── tsconfig-base.json ├── tsconfig-esm.json └── tsconfig.json /.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: [16.x, 18.x, 19.x] 10 | platform: 11 | - os: ubuntu-latest 12 | shell: bash 13 | fail-fast: false 14 | 15 | runs-on: ${{ matrix.platform.os }} 16 | defaults: 17 | run: 18 | shell: ${{ matrix.platform.shell }} 19 | 20 | steps: 21 | - name: Checkout Repository 22 | uses: actions/checkout@v1.1.0 23 | 24 | - name: Use Nodejs ${{ matrix.node-version }} 25 | uses: actions/setup-node@v1 26 | with: 27 | node-version: ${{ matrix.node-version }} 28 | 29 | - name: Install dependencies 30 | run: npm install 31 | 32 | - name: Run Tests 33 | run: npm test -- -c -t0 34 | -------------------------------------------------------------------------------- /.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 | /node_modules/ 2 | /coverage/ 3 | /.nyc_output/ 4 | /nyc_output/ 5 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # 4.0 2 | 3 | - Port to TypeScript 4 | - Replace default export with named `Domain` export 5 | - Build hybrid esm/cjs module 6 | 7 | # 3.0 8 | 9 | - Port to new interfaces 10 | - add types 11 | - drop support for node less than 16 12 | 13 | # 2.0 14 | 15 | - Drop support for node less than 10 16 | - Remove promiseExecutionId reset in `after` hook 17 | - gracefully no-op things if `process` is not set 18 | 19 | # 1.1 20 | 21 | - expose type on `onerror` method 22 | - add debug environment flag 23 | - report TypeError from ctor more helpfully 24 | 25 | # 1.0 26 | 27 | - initial implementation 28 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The ISC License 2 | 3 | Copyright (c) 2019-2023 Isaac Z. Schlueter and Contributors 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 | # async-hook-domain 2 | 3 | An implementation of the error-handling properties of the 4 | deprecated `domain` node core module, re-implemented on top of 5 | [`async_hooks`](https://nodejs.org/api/async_hooks.html). 6 | 7 | ## USAGE 8 | 9 | ```js 10 | // hybrid module, either works 11 | import { Domain } from 'async-hook-domain' 12 | // or: const { Domain } = require('async-hook-domain') 13 | 14 | // instantiating a Domain attaches it to the current async execution 15 | // context, and all child contexts that come off of it. You don't have 16 | // to call d.enter() or d.run(cb), just instantiate and it's done. 17 | // Pass an error-handling function to the constructor. This function 18 | // will be called whenever there is an uncaught exception or an 19 | // unhandled Promise rejection. 20 | 21 | const d = new Domain(er => { 22 | console.log('caught an error', er) 23 | // if you re-throw, it's not going to be caught, and will probably 24 | // cause the process to crash. 25 | }) 26 | 27 | setTimeout(() => { 28 | throw new Error('this is caught by the domain a few lines up') 29 | }) 30 | 31 | process.nextTick(() => { 32 | const d2 = new Domain(er => { 33 | console.log('any contexts spawned from this nextTick are caught here', er) 34 | // only catch one error. The next one will go to the parent 35 | d2.destroy() 36 | }) 37 | fs.readFile('does not exist', (er, data) => { 38 | if (er) throw er 39 | }) 40 | fs.readFile('also does not exist', (er, data) => { 41 | if (er) throw er 42 | }) 43 | }) 44 | 45 | // Adding a new domain here in the same context as the d above will 46 | // take over for this current context, as well as any that are created 47 | // from now on. But it won't affect the setTimeout above, because that 48 | // async context was created before this domain existed. 49 | const d3 = new Domain(er => console.log('d3', er)) 50 | 51 | // Unhandled promise rejections are handled, too. 52 | Promise.reject(new Error('this will be handled by d3')) 53 | 54 | // since a Promise rejection is an async hop, if we destroyed it right 55 | // now, it would not be there to catch the Promise.reject event. 56 | setTimeout(() => { 57 | // destroying d3 makes it like it never happened, so this will 58 | // be handled by the parent domain we created at the outset. 59 | d3.destroy() 60 | throw new Error('this will be handled by the parent') 61 | }) 62 | 63 | // When all domains are destroyed either manually or by virtue of their 64 | // async execution contexts being completed, or if no domain is active 65 | // for the current execution context, then it reverts back to normal 66 | // operation, with all event handlers removed and everything cleaned up. 67 | setTimeout(() => { 68 | d.destroy() 69 | throw new Error('this crashes the process like normal') 70 | }, 500) // time for the other contexts to wrap up 71 | ``` 72 | 73 | If you want to limit a Domain to a narrower scope, you can use 74 | node's 75 | [`AsyncResource`](https://nodejs.org/api/async_hooks.html#async_hooks_class_asyncresource) 76 | class, and instantiate the domain within its 77 | `runInAsyncScope(cb)` method. From then on, the domain will only 78 | be active when running from that Async Resource's scope. 79 | 80 | ## Important `new Promise()` Construction Method Caveat 81 | 82 | If you create a domain within a `Promise` construction method, 83 | then rejections of that promise will only be handled by the 84 | domain that was active when the Promise constructor was 85 | instantiated, and _not_ the new domain you create within the 86 | constructor. 87 | 88 | This is because, even though the _rejection_ happens later, and 89 | any throws are deferred until that time, the Promise construction 90 | method _itself_ is run synchronously. So, the 91 | `executionAsyncId()` in that context is still the same as it was 92 | when the Promise constructor was initiated. 93 | 94 | For example: 95 | 96 | ```js 97 | import { Domain } from 'async-hook-domain' 98 | 99 | const d1 = new Domain(() => console.log('handled by d1')) 100 | new Promise((_, reject) => { // <-- Promise bound to d1 domain 101 | 102 | // executionAsyncId identical to outside the Promise constructor 103 | 104 | // domains created later have no effect, Promise already bound, 105 | // as it was created at the instant of calling new Promise() 106 | // this is actually a new domain handling any subsequent throws 107 | // in the *parent* context! confusing! 108 | const d2 = new Domain(() => console.log('handled by d2')) 109 | 110 | // timeout created in d2's context, *sibling* of eventual 111 | // promise resolution/rejection 112 | setTimeout(() => { 113 | // d3 created as child of d2, but nothing bound to it 114 | // would handle any new async behaviors triggered by 115 | // the setTimeout's async context 116 | const d3 = new Domain(() => console.log('handled by d3')) 117 | 118 | // rejection occurs in child context, triggered by 119 | // execution context where new Promise was initiated. 120 | reject(new Error('will be handled by d1!')) 121 | }) 122 | }) 123 | ``` 124 | 125 | Since Promise construction happens synchronously in the same 126 | `executionAsyncId()` contex as outside the function, domains 127 | created within that context are as if they were created outside 128 | of the Promise constructor, and will stack up for that context. 129 | 130 | For example: 131 | 132 | ```js 133 | import { Domain } from 'async-hook-domain' 134 | 135 | // executionAsyncId=1, domain added 136 | const d1 = new Domain(() => console.log('handled by d1')) 137 | new Promise((_, reject) => { 138 | // still executionAsyncId=1, new child domain takes over 139 | // this is the new active domain for executionAsyncId=1, 140 | // even outside the Promise constructor! 141 | const d2 = new Domain(() => console.log('handled by d2')) 142 | // setTimeout creates new executionAsyncId=3, bound to d2 143 | setTimeout(() => { 144 | // executionAsyncId=3, d3 handling any errors in it 145 | const d3 = new Domain(() => console.log('handled by d3')) 146 | // resolve happens in executionAsyncId=2, the promise 147 | // resolution context triggered by the new Promise call 148 | resolve('this is fine') 149 | }) 150 | }) 151 | 152 | // throw happens in executionAsyncId=1, current domain is d2! 153 | throw new Error('will be handled by d2!') 154 | ``` 155 | 156 | Note that since a throw within a `Promise` construction method is 157 | treated as a call to `reject()`, this also applies to thrown 158 | errors within the construction method: 159 | 160 | ```js 161 | import { Domain } from 'async-hook-domain' 162 | const d1 = new Domain(() => console.error('handled by d1')) 163 | new Promise((_, reject) => { 164 | const d2 = new Domain(() => console.error('handled by d2')) 165 | 166 | throw 'this will be handled by d1, not d2!' 167 | }) 168 | ``` 169 | 170 | The execution context of the Promise itself is bound to the 171 | domain that was active at the time the Promise constructor 172 | _started_, so any rejection will be handled by that domain. 173 | 174 | If this all sounds confusing and very deep in the weeds, a safe 175 | approach is to never create a new `Domain` within a Promise 176 | construction function. Then everything will behave as you'd 177 | expect. 178 | 179 | I have explored the space here thoroughly, because this strikes 180 | me as counter-intuitive. As a user, I'd expect that a new domain 181 | created in a Promise constructor method would be a child of the 182 | domain that binds _to the Promise resolution_, and thus take over 183 | handling the subsequent Promise rejection, rather than binding to 184 | the context outside the Promise constructor. 185 | 186 | But that isn't how it works, and as of version 19, Node.js and v8 187 | do not provide adequate API surface to make it behave that way 188 | without making _other_ behavior less reliable. A future 189 | SemVer-major change will address this caveat when and if it 190 | becomes possible to do so. 191 | 192 | ## API 193 | 194 | ### `process.env.ASYNC_HOOK_DOMAIN_DEBUG = '1'` 195 | 196 | Set the `ASYNC_HOOK_DOMAIN_DEBUG` environment variable to `'1'` 197 | to print a lot of debugging information to stderr. 198 | 199 | ### const d = new Domain(errorHandlerFunction(error, type)) 200 | 201 | Create a new Domain and assign it to the current execution 202 | context and all child contexts that the current one triggers. 203 | 204 | The handler function is called with two arguments. The first is 205 | the error that was thrown or the rejection value of the rejected 206 | Promise. The second is either `'uncaughtException'` or 207 | `'unhandledRejection'`, depending on the type of event that 208 | raised the error. 209 | 210 | Note that even if the Domain prevents the process from failing 211 | entirely, Node.js _may_ still print a warning about unhandled 212 | rejections, depending on the `--unhandled-rejections` option. 213 | 214 | ### d.parent Domain 215 | 216 | If a Domain is already assigned to the current context on 217 | creation, then the current Domain set as the new Domain's 218 | `parent`. On destruction, any of a Domain's still-active 219 | execution contexts are assigned to its parent. 220 | 221 | ### d.onerror Function 222 | 223 | The `errorHandlerFunction` passed into the constructor. Called 224 | when an uncaughtException or unhandledRejection occurs in the 225 | scope of the Domain. 226 | 227 | If this function throws, then the domain will be destroyed, and 228 | the thrown error will be raised. If the domain doesn't have a 229 | parent, then this will likely crash the process entirely. 230 | 231 | ### d.destroyed Boolean 232 | 233 | Set to `true` if the domain is destroyed. 234 | 235 | ### d.ids Set 236 | 237 | A set of the `executionAsyncId` values corresponding to the 238 | execution contexts for which this Domain handles errors. 239 | 240 | ### d.destroy() Function 241 | 242 | Call to destroy the domain. This removes it from the system 243 | entirely, assigning any outstanding ids to its parent, if it has 244 | one, or leaving them uncovered if not. 245 | 246 | This is called implicitly when the domain's last covered 247 | execution context is destroyed, since at that point, the domain 248 | is unreachable anyway. 249 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "async-hook-domain", 3 | "version": "4.0.1", 4 | "description": "An implementation of Domain-like error handling, built on async_hooks", 5 | "main": "./dist/cjs/index.js", 6 | "types": "./dist/cjs/index.d.ts", 7 | "module": "./dist/mjs/index.js", 8 | "files": [ 9 | "dist" 10 | ], 11 | "exports": { 12 | "./package.json": { 13 | "import": "./package.json", 14 | "require": "./package.json" 15 | }, 16 | ".": { 17 | "import": { 18 | "types": "./dist/mjs/index.d.ts", 19 | "default": "./dist/mjs/index.js" 20 | }, 21 | "require": { 22 | "types": "./dist/cjs/index.d.ts", 23 | "default": "./dist/cjs/index.js" 24 | } 25 | } 26 | }, 27 | "scripts": { 28 | "preversion": "npm test", 29 | "postversion": "npm publish", 30 | "prepublishOnly": "git push origin --follow-tags", 31 | "preprepare": "rm -rf dist", 32 | "prepare": "tsc -p tsconfig.json && tsc -p tsconfig-esm.json && bash ./scripts/fixup.sh", 33 | "pretest": "npm run prepare", 34 | "presnap": "npm run prepare", 35 | "test": "c8 tap test/fixtures", 36 | "snap": "c8 tap test/fixtures", 37 | "format": "prettier --write . --loglevel warn", 38 | "typedoc": "typedoc --tsconfig tsconfig-esm.json ./lib/*.*ts" 39 | }, 40 | "devDependencies": { 41 | "@npmcli/promise-spawn": "^2.0.1", 42 | "@types/node": "^20.2.1", 43 | "@types/npmcli__promise-spawn": "^6.0.0", 44 | "@types/tap": "^15.0.8", 45 | "@types/uuid": "^9.0.1", 46 | "c8": "^7.13.0", 47 | "diff": "^5.0.0", 48 | "prettier": "^2.6.2", 49 | "tap": "^16.3.0", 50 | "ts-node": "^10.9.1", 51 | "typescript": "^5.0.4" 52 | }, 53 | "prettier": { 54 | "semi": false, 55 | "printWidth": 80, 56 | "tabWidth": 2, 57 | "useTabs": false, 58 | "singleQuote": true, 59 | "jsxSingleQuote": false, 60 | "bracketSameLine": true, 61 | "arrowParens": "avoid", 62 | "endOfLine": "lf" 63 | }, 64 | "tap": { 65 | "coverage": false, 66 | "node-arg": [ 67 | "--enable-source-maps", 68 | "--no-warnings", 69 | "--loader", 70 | "ts-node/esm", 71 | "test/run.js" 72 | ], 73 | "ts": false 74 | }, 75 | "repository": { 76 | "type": "git", 77 | "url": "git://github.com/tapjs/async-hook-domain.git" 78 | }, 79 | "keywords": [ 80 | "async", 81 | "hooks", 82 | "async_hooks", 83 | "domain", 84 | "error", 85 | "handling", 86 | "handler", 87 | "uncaughtException", 88 | "unhandledRejection", 89 | "catch", 90 | "promise", 91 | "execution", 92 | "context" 93 | ], 94 | "author": "Isaac Z. Schlueter (https://blog.izs.me)", 95 | "license": "ISC", 96 | "engines": { 97 | "node": ">=16" 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /scripts/fixup.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | rm -rf dist 4 | mv dist-tmp dist 5 | 6 | cat >dist/cjs/package.json <dist/mjs/package.json < { 20 | const p = proc as NodeJS.Process & { _handler: any } 21 | return !!p._handler 22 | }, 23 | setUncaughtExceptionCaptureCallback: (fn: Function) => { 24 | const p = proc as NodeJS.Process & { _handler: any } 25 | p._handler = fn 26 | }, 27 | /* c8 ignore end */ 28 | listeners: () => ({}), 29 | emit: () => false, 30 | once: () => proc, 31 | on: () => proc, 32 | removeListener: () => proc, 33 | } as unknown as NodeJS.Process) 34 | 35 | import { writeSync } from 'fs' 36 | import { format } from 'util' 37 | const debugAlways = (() => { 38 | return (...args: any[]) => writeSync(2, format(...args) + '\n') 39 | })() 40 | const debug = proc.env.ASYNC_HOOK_DOMAIN_DEBUG !== '1' ? () => {} : debugAlways 41 | 42 | const domains = new Map() 43 | 44 | // possible values here: 45 | // throw (default) 46 | // we let our rejection handler call the domain handler 47 | // none, warn-with-error-code 48 | // same as default 49 | // warn 50 | // same as default (no way to make it any less noisy, sadly) 51 | // strict 52 | // set the uncaughtExceptionMonitor, because it will throw, 53 | // but do NOT set our rejection handler, or it'll double-handle 54 | const unhandledRejectionMode = (() => { 55 | let mode = 'throw' 56 | for (let i = 0; i < proc.execArgv.length; i++) { 57 | const m = process.execArgv[i] 58 | if (m.startsWith('--unhandled-rejections=')) { 59 | mode = m.substring('--unhandled-rejections='.length) 60 | } else if (m === '--unhandled-rejections') { 61 | mode = proc.execArgv[i + 1] 62 | } 63 | } 64 | return mode 65 | })() 66 | 67 | // the async hook activation and deactivation 68 | let domainHook: AsyncHook | null = null 69 | const activateDomains = () => { 70 | if (!domainHook) { 71 | debug('ACTIVATE') 72 | domainHook = createHook(hookMethods) 73 | domainHook.enable() 74 | proc.on('uncaughtExceptionMonitor', domainErrorHandler) 75 | if (unhandledRejectionMode !== 'strict') { 76 | proc.emit = domainProcessEmit as NodeJS.Process['emit'] 77 | } 78 | } 79 | } 80 | const deactivateDomains = () => { 81 | if (domainHook) { 82 | debug('DEACTIVATE') 83 | domainHook.disable() 84 | domainHook = null 85 | proc.removeListener('uncaughtExceptionMonitor', domainErrorHandler) 86 | proc.emit = originalProcessEmit 87 | } 88 | } 89 | 90 | // monkey patch to silently listen on unhandledRejection, without 91 | // marking the event as 'handled' unless we handled it. 92 | // Do nothing if there's a user handler for the event, though. 93 | const originalProcessEmit = proc.emit 94 | const domainProcessEmit = (ev: string | symbol, ...args: any[]) => { 95 | if ( 96 | ev !== 'unhandledRejection' || 97 | proc.listeners('unhandledRejection').length 98 | ) { 99 | //@ts-ignore 100 | return originalProcessEmit.call(proc, ev, ...args) 101 | } 102 | const er = args[0] 103 | return domainErrorHandler(er, 'unhandledRejection', true) 104 | } 105 | 106 | const domainErrorHandler = ( 107 | er: unknown, 108 | ev?: string, 109 | rejectionHandler: boolean = false 110 | ) => { 111 | debug('AHD MAYBE HANDLE?', ev, er) 112 | // if anything else attached a handler, then it's their problem, 113 | // not ours. get out of the way. 114 | if ( 115 | proc.hasUncaughtExceptionCaptureCallback() || 116 | proc.listeners('uncaughtException').length > 0 117 | ) { 118 | debug('OTHER HANDLER ALREADY SET') 119 | return false 120 | } 121 | const domain = currentDomain() 122 | if (domain) { 123 | debug('HAVE DOMAIN') 124 | try { 125 | domain.onerror(er, ev) 126 | } catch (e) { 127 | debug('ONERROR THREW', e) 128 | domain.destroy() 129 | // this is pretty bad. treat it as a fatal exception, which 130 | // may or may not be caught in the next domain up. 131 | // We drop 'from promise', because now it's a throw. 132 | if (domainErrorHandler(e)) { 133 | return true 134 | } 135 | throw e 136 | } 137 | // at this point, we presumably handled the error, and attach a 138 | // no-op one-time handler to just prevent the crash from happening. 139 | if (!rejectionHandler) { 140 | proc.setUncaughtExceptionCaptureCallback(() => { 141 | debug('UECC ONCE') 142 | proc.setUncaughtExceptionCaptureCallback(null) 143 | }) 144 | // in strict mode, node raises the error *before* the uR event, 145 | // and it warns if the uR event is not handled. 146 | if (unhandledRejectionMode === 'strict') { 147 | process.once('unhandledRejection', () => {}) 148 | } 149 | } 150 | return true 151 | } 152 | return false 153 | } 154 | 155 | // the hook callbacks 156 | const hookMethods: HookCallbacks = { 157 | init(id, type, triggerId) { 158 | debug('INIT', id, type, triggerId) 159 | const current = domains.get(triggerId) 160 | if (current) { 161 | debug('INIT have current', current) 162 | current.ids.add(id) 163 | domains.set(id, current) 164 | debug('POST INIT', id, type, current) 165 | } 166 | }, 167 | 168 | destroy(id) { 169 | const domain = domains.get(id) 170 | debug('DESTROY', id) 171 | if (!domain) { 172 | return 173 | } 174 | domains.delete(id) 175 | domain.ids.delete(id) 176 | if (!domain.ids.size) { 177 | domain.destroy() 178 | } 179 | }, 180 | } 181 | 182 | const currentDomain = () => domains.get(executionAsyncId()) 183 | 184 | let id = 1 185 | export class Domain { 186 | eid: number 187 | id: number 188 | ids: Set 189 | onerror: ( 190 | er: unknown, 191 | event: 'uncaughtException' | 'unhandledRejection' 192 | ) => any 193 | parent?: Domain 194 | destroyed: boolean 195 | 196 | constructor( 197 | onerror: ( 198 | er: unknown, 199 | event: 'uncaughtException' | 'unhandledRejection' 200 | ) => any 201 | ) { 202 | if (typeof onerror !== 'function') { 203 | // point at where the wrong thing was actually done 204 | const er = new TypeError('onerror must be a function') 205 | Error.captureStackTrace(er, this.constructor) 206 | throw er 207 | } 208 | const eid = executionAsyncId() 209 | this.eid = eid 210 | this.id = id++ 211 | this.ids = new Set([eid]) 212 | this.onerror = onerror 213 | this.parent = domains.get(eid) 214 | this.destroyed = false 215 | domains.set(eid, this) 216 | debug('NEW DOMAIN', this.id, this.eid, this.ids) 217 | activateDomains() 218 | } 219 | 220 | destroy() { 221 | if (this.destroyed) { 222 | return 223 | } 224 | debug('DESTROY DOMAIN', this.id, this.eid, this.ids) 225 | this.destroyed = true 226 | // find the nearest non-destroyed parent, assign all ids to it 227 | let parent = this.parent 228 | while (parent && parent.destroyed) { 229 | parent = parent.parent 230 | } 231 | this.parent = parent 232 | if (parent) { 233 | for (const id of this.ids) { 234 | domains.set(id, parent) 235 | parent.ids.add(id) 236 | } 237 | } else { 238 | for (const id of this.ids) { 239 | domains.delete(id) 240 | } 241 | } 242 | this.ids = new Set() 243 | if (!domains.size) { 244 | deactivateDomains() 245 | } 246 | } 247 | } 248 | -------------------------------------------------------------------------------- /tap-snapshots/test/run.js-test-fixtures-basic.js.test.cjs: -------------------------------------------------------------------------------- 1 | /* IMPORTANT 2 | * This snapshot file is auto-generated, but designed for humans. 3 | * It should be checked into source control and tracked carefully. 4 | * Re-generate by setting TAP_SNAPSHOT=1 and running tests. 5 | * Make sure to inspect the output below. Do not ignore changes! 6 | */ 7 | 'use strict' 8 | exports[`test/run.js test/fixtures/basic.js TAP > error 1`] = ` 9 | null 10 | 11 | ` 12 | 13 | exports[`test/run.js test/fixtures/basic.js TAP > output 1`] = ` 14 | caught: catch this uncaughtException 15 | caught: timeout throw uncaughtException 16 | 17 | ` 18 | 19 | exports[`test/run.js test/fixtures/basic.js TAP > stderr 1`] = ` 20 | 21 | 22 | ` 23 | -------------------------------------------------------------------------------- /tap-snapshots/test/run.js-test-fixtures-child.js.test.cjs: -------------------------------------------------------------------------------- 1 | /* IMPORTANT 2 | * This snapshot file is auto-generated, but designed for humans. 3 | * It should be checked into source control and tracked carefully. 4 | * Re-generate by setting TAP_SNAPSHOT=1 and running tests. 5 | * Make sure to inspect the output below. Do not ignore changes! 6 | */ 7 | 'use strict' 8 | exports[`test/run.js test/fixtures/child.js TAP > error 1`] = ` 9 | null 10 | 11 | ` 12 | 13 | exports[`test/run.js test/fixtures/child.js TAP > output 1`] = ` 14 | parent: throw in main context 15 | child: sync throw in child context 16 | child: setImmediate throw 17 | parent: throw after destroy, go to parent 18 | 19 | ` 20 | 21 | exports[`test/run.js test/fixtures/child.js TAP > stderr 1`] = ` 22 | 23 | 24 | ` 25 | -------------------------------------------------------------------------------- /tap-snapshots/test/run.js-test-fixtures-debug.js.test.cjs: -------------------------------------------------------------------------------- 1 | /* IMPORTANT 2 | * This snapshot file is auto-generated, but designed for humans. 3 | * It should be checked into source control and tracked carefully. 4 | * Re-generate by setting TAP_SNAPSHOT=1 and running tests. 5 | * Make sure to inspect the output below. Do not ignore changes! 6 | */ 7 | 'use strict' 8 | exports[`test/run.js test/fixtures/debug.js TAP > error 1`] = ` 9 | null 10 | 11 | ` 12 | 13 | exports[`test/run.js test/fixtures/debug.js TAP > output 1`] = ` 14 | 15 | 16 | ` 17 | 18 | exports[`test/run.js test/fixtures/debug.js TAP > stderr 1`] = ` 19 | NEW DOMAIN 1 1 Set(1) { 1 } 20 | ACTIVATE 21 | DESTROY DOMAIN 1 1 Set(1) { 1 } 22 | DEACTIVATE 23 | 24 | ` 25 | -------------------------------------------------------------------------------- /tap-snapshots/test/run.js-test-fixtures-destroy.js.test.cjs: -------------------------------------------------------------------------------- 1 | /* IMPORTANT 2 | * This snapshot file is auto-generated, but designed for humans. 3 | * It should be checked into source control and tracked carefully. 4 | * Re-generate by setting TAP_SNAPSHOT=1 and running tests. 5 | * Make sure to inspect the output below. Do not ignore changes! 6 | */ 7 | 'use strict' 8 | exports[`test/run.js test/fixtures/destroy.js TAP > error 1`] = ` 9 | null 10 | 11 | ` 12 | 13 | exports[`test/run.js test/fixtures/destroy.js TAP > output 1`] = ` 14 | child 2 err 15 | child err 16 | c walk up to parent 17 | 18 | ` 19 | 20 | exports[`test/run.js test/fixtures/destroy.js TAP > stderr 1`] = ` 21 | 22 | 23 | ` 24 | -------------------------------------------------------------------------------- /tap-snapshots/test/run.js-test-fixtures-multiple-destroy.js.test.cjs: -------------------------------------------------------------------------------- 1 | /* IMPORTANT 2 | * This snapshot file is auto-generated, but designed for humans. 3 | * It should be checked into source control and tracked carefully. 4 | * Re-generate by setting TAP_SNAPSHOT=1 and running tests. 5 | * Make sure to inspect the output below. Do not ignore changes! 6 | */ 7 | 'use strict' 8 | exports[`test/run.js test/fixtures/multiple-destroy.js TAP > error 1`] = ` 9 | null 10 | 11 | ` 12 | 13 | exports[`test/run.js test/fixtures/multiple-destroy.js TAP > output 1`] = ` 14 | 15 | 16 | ` 17 | 18 | exports[`test/run.js test/fixtures/multiple-destroy.js TAP > stderr 1`] = ` 19 | grandparent 20 | 21 | ` 22 | -------------------------------------------------------------------------------- /tap-snapshots/test/run.js-test-fixtures-no-handler.js.test.cjs: -------------------------------------------------------------------------------- 1 | /* IMPORTANT 2 | * This snapshot file is auto-generated, but designed for humans. 3 | * It should be checked into source control and tracked carefully. 4 | * Re-generate by setting TAP_SNAPSHOT=1 and running tests. 5 | * Make sure to inspect the output below. Do not ignore changes! 6 | */ 7 | 'use strict' 8 | exports[`test/run.js test/fixtures/no-handler.js TAP > error 1`] = ` 9 | Object { 10 | "code": 1, 11 | "signal": null, 12 | } 13 | 14 | ` 15 | 16 | exports[`test/run.js test/fixtures/no-handler.js TAP > output 1`] = ` 17 | 18 | 19 | ` 20 | 21 | exports[`test/run.js test/fixtures/no-handler.js TAP > stderr 1`] = ` 22 | TypeError: onerror must be a function 23 | {STACK} 24 | 25 | ` 26 | -------------------------------------------------------------------------------- /tap-snapshots/test/run.js-test-fixtures-other-handler.js.test.cjs: -------------------------------------------------------------------------------- 1 | /* IMPORTANT 2 | * This snapshot file is auto-generated, but designed for humans. 3 | * It should be checked into source control and tracked carefully. 4 | * Re-generate by setting TAP_SNAPSHOT=1 and running tests. 5 | * Make sure to inspect the output below. Do not ignore changes! 6 | */ 7 | 'use strict' 8 | exports[`test/run.js test/fixtures/other-handler.js TAP > error 1`] = ` 9 | null 10 | 11 | ` 12 | 13 | exports[`test/run.js test/fixtures/other-handler.js TAP > output 1`] = ` 14 | p.uE bar 15 | p.uR foo 16 | 17 | ` 18 | 19 | exports[`test/run.js test/fixtures/other-handler.js TAP > stderr 1`] = ` 20 | 21 | 22 | ` 23 | -------------------------------------------------------------------------------- /tap-snapshots/test/run.js-test-fixtures-process-clubbing.js.test.cjs: -------------------------------------------------------------------------------- 1 | /* IMPORTANT 2 | * This snapshot file is auto-generated, but designed for humans. 3 | * It should be checked into source control and tracked carefully. 4 | * Re-generate by setting TAP_SNAPSHOT=1 and running tests. 5 | * Make sure to inspect the output below. Do not ignore changes! 6 | */ 7 | 'use strict' 8 | exports[`test/run.js test/fixtures/process-clubbing.js TAP > error 1`] = ` 9 | Object { 10 | "code": 1, 11 | "signal": null, 12 | } 13 | 14 | ` 15 | 16 | exports[`test/run.js test/fixtures/process-clubbing.js TAP > output 1`] = ` 17 | caught: thrown 18 | 19 | ` 20 | 21 | exports[`test/run.js test/fixtures/process-clubbing.js TAP > stderr 1`] = ` 22 | Error: this will not be caught 23 | {STACK} 24 | 25 | ` 26 | -------------------------------------------------------------------------------- /tap-snapshots/test/run.js-test-fixtures-process-missing.js.test.cjs: -------------------------------------------------------------------------------- 1 | /* IMPORTANT 2 | * This snapshot file is auto-generated, but designed for humans. 3 | * It should be checked into source control and tracked carefully. 4 | * Re-generate by setting TAP_SNAPSHOT=1 and running tests. 5 | * Make sure to inspect the output below. Do not ignore changes! 6 | */ 7 | 'use strict' 8 | exports[`test/run.js test/fixtures/process-missing.js TAP > error 1`] = ` 9 | Object { 10 | "code": 1, 11 | "signal": null, 12 | } 13 | 14 | ` 15 | 16 | exports[`test/run.js test/fixtures/process-missing.js TAP > output 1`] = ` 17 | 18 | 19 | ` 20 | 21 | exports[`test/run.js test/fixtures/process-missing.js TAP > stderr 1`] = ` 22 | thrown 23 | 24 | ` 25 | -------------------------------------------------------------------------------- /tap-snapshots/test/run.js-test-fixtures-promise-no-domain.js.test.cjs: -------------------------------------------------------------------------------- 1 | /* IMPORTANT 2 | * This snapshot file is auto-generated, but designed for humans. 3 | * It should be checked into source control and tracked carefully. 4 | * Re-generate by setting TAP_SNAPSHOT=1 and running tests. 5 | * Make sure to inspect the output below. Do not ignore changes! 6 | */ 7 | 'use strict' 8 | exports[`test/run.js test/fixtures/promise-no-domain.js TAP > error 1`] = ` 9 | null 10 | 11 | ` 12 | 13 | exports[`test/run.js test/fixtures/promise-no-domain.js TAP > output 1`] = ` 14 | d caught 1 15 | d caught 2 16 | 17 | ` 18 | 19 | exports[`test/run.js test/fixtures/promise-no-domain.js TAP > stderr 1`] = ` 20 | (node:{PID}) UnhandledPromiseRejectionWarning: no one to catch this 21 | (node:{PID}) UnhandledPromiseRejectionWarning: ... (rejection id: 1) 22 | (node:{PID}) UnhandledPromiseRejectionWarning: caught 1 23 | (node:{PID}) UnhandledPromiseRejectionWarning: ... (rejection id: 2) 24 | (node:{PID}) UnhandledPromiseRejectionWarning: happy 25 | (node:{PID}) UnhandledPromiseRejectionWarning: ... (rejection id: 3) 26 | (node:{PID}) UnhandledPromiseRejectionWarning: sad 27 | (node:{PID}) UnhandledPromiseRejectionWarning: ... (rejection id: 4) 28 | (node:{PID}) UnhandledPromiseRejectionWarning: ok 29 | (node:{PID}) UnhandledPromiseRejectionWarning: ... (rejection id: 5) 30 | (node:{PID}) UnhandledPromiseRejectionWarning: caught 2 31 | (node:{PID}) UnhandledPromiseRejectionWarning: ... (rejection id: 6) 32 | 33 | ` 34 | -------------------------------------------------------------------------------- /tap-snapshots/test/run.js-test-fixtures-promise-rejections-unhandled-none.js.test.cjs: -------------------------------------------------------------------------------- 1 | /* IMPORTANT 2 | * This snapshot file is auto-generated, but designed for humans. 3 | * It should be checked into source control and tracked carefully. 4 | * Re-generate by setting TAP_SNAPSHOT=1 and running tests. 5 | * Make sure to inspect the output below. Do not ignore changes! 6 | */ 7 | 'use strict' 8 | exports[`test/run.js test/fixtures/promise-rejections-unhandled-none.js TAP > error 1`] = ` 9 | null 10 | 11 | ` 12 | 13 | exports[`test/run.js test/fixtures/promise-rejections-unhandled-none.js TAP > output 1`] = ` 14 | happy timeout 15 | sad timeout 16 | 17 | ` 18 | 19 | exports[`test/run.js test/fixtures/promise-rejections-unhandled-none.js TAP > stderr 1`] = ` 20 | CAUGHT unhandledRejection happy 21 | CAUGHT unhandledRejection happy2 22 | 23 | ` 24 | -------------------------------------------------------------------------------- /tap-snapshots/test/run.js-test-fixtures-promise-rejections-unhandled-strict-with-space.js.test.cjs: -------------------------------------------------------------------------------- 1 | /* IMPORTANT 2 | * This snapshot file is auto-generated, but designed for humans. 3 | * It should be checked into source control and tracked carefully. 4 | * Re-generate by setting TAP_SNAPSHOT=1 and running tests. 5 | * Make sure to inspect the output below. Do not ignore changes! 6 | */ 7 | 'use strict' 8 | exports[`test/run.js test/fixtures/promise-rejections-unhandled-strict-with-space.js TAP > error 1`] = ` 9 | Object { 10 | "code": 1, 11 | "signal": null, 12 | } 13 | 14 | ` 15 | 16 | exports[`test/run.js test/fixtures/promise-rejections-unhandled-strict-with-space.js TAP > output 1`] = ` 17 | happy timeout 18 | 19 | ` 20 | 21 | exports[`test/run.js test/fixtures/promise-rejections-unhandled-strict-with-space.js TAP > stderr 1`] = ` 22 | CAUGHT unhandledRejection happy 23 | CAUGHT unhandledRejection happy2 24 | Error: sad 25 | {STACK} 26 | 27 | ` 28 | -------------------------------------------------------------------------------- /tap-snapshots/test/run.js-test-fixtures-promise-rejections-unhandled-strict.js.test.cjs: -------------------------------------------------------------------------------- 1 | /* IMPORTANT 2 | * This snapshot file is auto-generated, but designed for humans. 3 | * It should be checked into source control and tracked carefully. 4 | * Re-generate by setting TAP_SNAPSHOT=1 and running tests. 5 | * Make sure to inspect the output below. Do not ignore changes! 6 | */ 7 | 'use strict' 8 | exports[`test/run.js test/fixtures/promise-rejections-unhandled-strict.js TAP > error 1`] = ` 9 | Object { 10 | "code": 1, 11 | "signal": null, 12 | } 13 | 14 | ` 15 | 16 | exports[`test/run.js test/fixtures/promise-rejections-unhandled-strict.js TAP > output 1`] = ` 17 | happy timeout 18 | 19 | ` 20 | 21 | exports[`test/run.js test/fixtures/promise-rejections-unhandled-strict.js TAP > stderr 1`] = ` 22 | CAUGHT unhandledRejection happy 23 | CAUGHT unhandledRejection happy2 24 | Error: sad 25 | {STACK} 26 | 27 | ` 28 | -------------------------------------------------------------------------------- /tap-snapshots/test/run.js-test-fixtures-promise-rejections-unhandled-throw.js.test.cjs: -------------------------------------------------------------------------------- 1 | /* IMPORTANT 2 | * This snapshot file is auto-generated, but designed for humans. 3 | * It should be checked into source control and tracked carefully. 4 | * Re-generate by setting TAP_SNAPSHOT=1 and running tests. 5 | * Make sure to inspect the output below. Do not ignore changes! 6 | */ 7 | 'use strict' 8 | exports[`test/run.js test/fixtures/promise-rejections-unhandled-throw.js TAP > error 1`] = ` 9 | Object { 10 | "code": 1, 11 | "signal": null, 12 | } 13 | 14 | ` 15 | 16 | exports[`test/run.js test/fixtures/promise-rejections-unhandled-throw.js TAP > output 1`] = ` 17 | happy timeout 18 | 19 | ` 20 | 21 | exports[`test/run.js test/fixtures/promise-rejections-unhandled-throw.js TAP > stderr 1`] = ` 22 | CAUGHT unhandledRejection happy 23 | CAUGHT unhandledRejection happy2 24 | Error: sad 25 | {STACK} 26 | 27 | ` 28 | -------------------------------------------------------------------------------- /tap-snapshots/test/run.js-test-fixtures-promise-rejections-unhandled-warn-with-error-code.js.test.cjs: -------------------------------------------------------------------------------- 1 | /* IMPORTANT 2 | * This snapshot file is auto-generated, but designed for humans. 3 | * It should be checked into source control and tracked carefully. 4 | * Re-generate by setting TAP_SNAPSHOT=1 and running tests. 5 | * Make sure to inspect the output below. Do not ignore changes! 6 | */ 7 | 'use strict' 8 | exports[`test/run.js test/fixtures/promise-rejections-unhandled-warn-with-error-code.js TAP > error 1`] = ` 9 | Object { 10 | "code": 1, 11 | "signal": null, 12 | } 13 | 14 | ` 15 | 16 | exports[`test/run.js test/fixtures/promise-rejections-unhandled-warn-with-error-code.js TAP > output 1`] = ` 17 | happy timeout 18 | sad timeout 19 | 20 | ` 21 | 22 | exports[`test/run.js test/fixtures/promise-rejections-unhandled-warn-with-error-code.js TAP > stderr 1`] = ` 23 | CAUGHT unhandledRejection happy 24 | CAUGHT unhandledRejection happy2 25 | (node:{PID}) UnhandledPromiseRejectionWarning: Error: sad 26 | {STACK} 27 | (node:{PID}) UnhandledPromiseRejectionWarning: ... (rejection id: 3) 28 | 29 | ` 30 | -------------------------------------------------------------------------------- /tap-snapshots/test/run.js-test-fixtures-promise-rejections-unhandled-warn.js.test.cjs: -------------------------------------------------------------------------------- 1 | /* IMPORTANT 2 | * This snapshot file is auto-generated, but designed for humans. 3 | * It should be checked into source control and tracked carefully. 4 | * Re-generate by setting TAP_SNAPSHOT=1 and running tests. 5 | * Make sure to inspect the output below. Do not ignore changes! 6 | */ 7 | 'use strict' 8 | exports[`test/run.js test/fixtures/promise-rejections-unhandled-warn.js TAP > error 1`] = ` 9 | null 10 | 11 | ` 12 | 13 | exports[`test/run.js test/fixtures/promise-rejections-unhandled-warn.js TAP > output 1`] = ` 14 | happy timeout 15 | sad timeout 16 | 17 | ` 18 | 19 | exports[`test/run.js test/fixtures/promise-rejections-unhandled-warn.js TAP > stderr 1`] = ` 20 | CAUGHT unhandledRejection happy 21 | (node:{PID}) UnhandledPromiseRejectionWarning: Error: happy 22 | {STACK} 23 | (node:{PID}) UnhandledPromiseRejectionWarning: ... (rejection id: 1) 24 | CAUGHT unhandledRejection happy2 25 | (node:{PID}) UnhandledPromiseRejectionWarning: Error: happy2 26 | {STACK} 27 | (node:{PID}) UnhandledPromiseRejectionWarning: ... (rejection id: 2) 28 | (node:{PID}) UnhandledPromiseRejectionWarning: Error: sad 29 | {STACK} 30 | (node:{PID}) UnhandledPromiseRejectionWarning: ... (rejection id: 3) 31 | 32 | ` 33 | -------------------------------------------------------------------------------- /tap-snapshots/test/run.js-test-fixtures-promise-rejections.js.test.cjs: -------------------------------------------------------------------------------- 1 | /* IMPORTANT 2 | * This snapshot file is auto-generated, but designed for humans. 3 | * It should be checked into source control and tracked carefully. 4 | * Re-generate by setting TAP_SNAPSHOT=1 and running tests. 5 | * Make sure to inspect the output below. Do not ignore changes! 6 | */ 7 | 'use strict' 8 | exports[`test/run.js test/fixtures/promise-rejections.js TAP > error 1`] = ` 9 | Object { 10 | "code": 1, 11 | "signal": null, 12 | } 13 | 14 | ` 15 | 16 | exports[`test/run.js test/fixtures/promise-rejections.js TAP > output 1`] = ` 17 | happy timeout 18 | sad timeout 19 | 20 | ` 21 | 22 | exports[`test/run.js test/fixtures/promise-rejections.js TAP > stderr 1`] = ` 23 | CAUGHT unhandledRejection happy 24 | CAUGHT unhandledRejection happy2 25 | (node:{PID}) UnhandledPromiseRejectionWarning: Error: sad 26 | {STACK} 27 | (node:{PID}) UnhandledPromiseRejectionWarning: ... (rejection id: 3) 28 | 29 | ` 30 | -------------------------------------------------------------------------------- /tap-snapshots/test/run.js-test-fixtures-promise-throwing-handler.js.test.cjs: -------------------------------------------------------------------------------- 1 | /* IMPORTANT 2 | * This snapshot file is auto-generated, but designed for humans. 3 | * It should be checked into source control and tracked carefully. 4 | * Re-generate by setting TAP_SNAPSHOT=1 and running tests. 5 | * Make sure to inspect the output below. Do not ignore changes! 6 | */ 7 | 'use strict' 8 | exports[`test/run.js test/fixtures/promise-throwing-handler.js TAP > error 1`] = ` 9 | Object { 10 | "code": 1, 11 | "signal": null, 12 | } 13 | 14 | ` 15 | 16 | exports[`test/run.js test/fixtures/promise-throwing-handler.js TAP > output 1`] = ` 17 | 18 | 19 | ` 20 | 21 | exports[`test/run.js test/fixtures/promise-throwing-handler.js TAP > stderr 1`] = ` 22 | Error: er 23 | {STACK} 24 | 25 | ` 26 | -------------------------------------------------------------------------------- /tap-snapshots/test/run.js-test-fixtures-promise-timing.js.test.cjs: -------------------------------------------------------------------------------- 1 | /* IMPORTANT 2 | * This snapshot file is auto-generated, but designed for humans. 3 | * It should be checked into source control and tracked carefully. 4 | * Re-generate by setting TAP_SNAPSHOT=1 and running tests. 5 | * Make sure to inspect the output below. Do not ignore changes! 6 | */ 7 | 'use strict' 8 | exports[`test/run.js test/fixtures/promise-timing.js TAP > error 1`] = ` 9 | null 10 | 11 | ` 12 | 13 | exports[`test/run.js test/fixtures/promise-timing.js TAP > output 1`] = ` 14 | 15 | 16 | ` 17 | 18 | exports[`test/run.js test/fixtures/promise-timing.js TAP > stderr 1`] = ` 19 | call new Promise 20 | in unchained promise 21 | schedule timeout to rejection 22 | done with scheduling, trigger immediate rejection 23 | reject immediately 24 | in chained promise 25 | throw num 26 | domain 6 happy - caught by domain 6 27 | domain 5 1234 expect caught by domain 5 28 | rejection in timeout, expect caught by domain 1 29 | domain 1 rejection should be caught by domain 1 30 | 31 | ` 32 | -------------------------------------------------------------------------------- /tap-snapshots/test/run.js-test-fixtures-promise.js.test.cjs: -------------------------------------------------------------------------------- 1 | /* IMPORTANT 2 | * This snapshot file is auto-generated, but designed for humans. 3 | * It should be checked into source control and tracked carefully. 4 | * Re-generate by setting TAP_SNAPSHOT=1 and running tests. 5 | * Make sure to inspect the output below. Do not ignore changes! 6 | */ 7 | 'use strict' 8 | exports[`test/run.js test/fixtures/promise.js TAP > error 1`] = ` 9 | null 10 | 11 | ` 12 | 13 | exports[`test/run.js test/fixtures/promise.js TAP > output 1`] = ` 14 | d happy unhandledRejection 15 | d 1234 unhandledRejection 16 | d rejection unhandledRejection 17 | 18 | ` 19 | 20 | exports[`test/run.js test/fixtures/promise.js TAP > stderr 1`] = ` 21 | 22 | 23 | ` 24 | -------------------------------------------------------------------------------- /tap-snapshots/test/run.js-test-fixtures-throw-in-handler.js.test.cjs: -------------------------------------------------------------------------------- 1 | /* IMPORTANT 2 | * This snapshot file is auto-generated, but designed for humans. 3 | * It should be checked into source control and tracked carefully. 4 | * Re-generate by setting TAP_SNAPSHOT=1 and running tests. 5 | * Make sure to inspect the output below. Do not ignore changes! 6 | */ 7 | 'use strict' 8 | exports[`test/run.js test/fixtures/throw-in-handler.js TAP > error 1`] = ` 9 | Object { 10 | "code": 7, 11 | "signal": null, 12 | } 13 | 14 | ` 15 | 16 | exports[`test/run.js test/fixtures/throw-in-handler.js TAP > output 1`] = ` 17 | 18 | 19 | ` 20 | 21 | exports[`test/run.js test/fixtures/throw-in-handler.js TAP > stderr 1`] = ` 22 | Error: errr 23 | {STACK} 24 | 25 | ` 26 | -------------------------------------------------------------------------------- /tap-snapshots/test/run.js-test-fixtures-uncaught.js.test.cjs: -------------------------------------------------------------------------------- 1 | /* IMPORTANT 2 | * This snapshot file is auto-generated, but designed for humans. 3 | * It should be checked into source control and tracked carefully. 4 | * Re-generate by setting TAP_SNAPSHOT=1 and running tests. 5 | * Make sure to inspect the output below. Do not ignore changes! 6 | */ 7 | 'use strict' 8 | exports[`test/run.js test/fixtures/uncaught.js TAP > error 1`] = ` 9 | Object { 10 | "code": 1, 11 | "signal": null, 12 | } 13 | 14 | ` 15 | 16 | exports[`test/run.js test/fixtures/uncaught.js TAP > output 1`] = ` 17 | caught: thrown 18 | 19 | ` 20 | 21 | exports[`test/run.js test/fixtures/uncaught.js TAP > stderr 1`] = ` 22 | Error: this will not be caught 23 | {STACK} 24 | 25 | ` 26 | -------------------------------------------------------------------------------- /test/fixtures/basic.js: -------------------------------------------------------------------------------- 1 | const { Domain } = require('../..') 2 | 3 | const d = new Domain((e, type) => console.log('caught:', e, type)) 4 | setTimeout(() => { 5 | throw 'timeout throw' 6 | }) 7 | throw 'catch this' 8 | -------------------------------------------------------------------------------- /test/fixtures/child.js: -------------------------------------------------------------------------------- 1 | const { Domain } = require('../..') 2 | 3 | const d = new Domain(e => console.log('parent:', e)) 4 | 5 | let d2 6 | process.nextTick(() => { 7 | d2 = new Domain(e => console.log('child:', e)) 8 | setImmediate(() => { 9 | throw 'setImmediate throw' 10 | }) 11 | setTimeout(() => { 12 | d2.destroy() 13 | throw 'throw after destroy, go to parent' 14 | }, 100) 15 | throw 'sync throw in child context' 16 | }) 17 | 18 | throw 'throw in main context' 19 | -------------------------------------------------------------------------------- /test/fixtures/debug.js: -------------------------------------------------------------------------------- 1 | process.env.ASYNC_HOOK_DOMAIN_DEBUG = '1' 2 | const { Domain } = require('../..') 3 | const d = new Domain(() => {}) 4 | d.destroy() 5 | -------------------------------------------------------------------------------- /test/fixtures/destroy.js: -------------------------------------------------------------------------------- 1 | const { Domain } = require('../..') 2 | 3 | // shouldn't catch anything 4 | const d = new Domain(er => { throw er }) 5 | d.destroy() 6 | // second time does nothing 7 | d.destroy() 8 | // or does it????!?? 9 | d.destroyed = false 10 | d.destroy() 11 | 12 | const root = new Domain(er => console.log('root', er)) 13 | 14 | setImmediate(() => { 15 | const child = new Domain(er => console.log('child', er)) 16 | setImmediate(() => { 17 | const child2 = new Domain(er => { 18 | console.log('child 2', er) 19 | child2.destroy() 20 | }) 21 | setImmediate(() => { throw 'err' }) 22 | setImmediate(() => { throw 'err' }) 23 | }) 24 | }) 25 | 26 | setTimeout(() => { 27 | const c = new Domain(er => console.log('c', er)) 28 | setTimeout(() => { 29 | const d = new Domain(er => console.log('d', er)) 30 | setTimeout(() => { 31 | const e = new Domain(er => console.log('e', er)) 32 | setTimeout(() => { 33 | const f = new Domain(er => console.log('f', er)) 34 | d.destroy() 35 | e.destroy() 36 | setTimeout(() => { 37 | throw 'walk up to parent' 38 | }) 39 | f.destroy() 40 | }) 41 | }) 42 | }) 43 | }, 100) 44 | -------------------------------------------------------------------------------- /test/fixtures/multiple-destroy.js: -------------------------------------------------------------------------------- 1 | const { Domain } = require('../..') 2 | const grandparent = new Domain(er => console.error('grandparent')) 3 | const parent = new Domain(er => { throw er }) 4 | setTimeout(() => { 5 | const child = new Domain(er => { throw er }) 6 | setTimeout(() => { 7 | setTimeout(() => { 8 | setTimeout(() => { 9 | throw new Error('yolo') 10 | }) 11 | }) 12 | }) 13 | }) 14 | -------------------------------------------------------------------------------- /test/fixtures/no-handler.js: -------------------------------------------------------------------------------- 1 | const { Domain } = require('../..') 2 | new Domain() 3 | -------------------------------------------------------------------------------- /test/fixtures/other-handler.js: -------------------------------------------------------------------------------- 1 | const { Domain } = require('../..') 2 | 3 | process.on('uncaughtException', er => 4 | console.log('p.uE', er)) 5 | 6 | process.on('unhandledRejection', er => 7 | console.log('p.uR', er)) 8 | 9 | setTimeout(() => { 10 | const d = new Domain(er => console.log('d', er)) 11 | setTimeout(() => {}, 100) 12 | }) 13 | setTimeout(() => { 14 | Promise.reject('foo') 15 | throw 'bar' 16 | }, 50) 17 | -------------------------------------------------------------------------------- /test/fixtures/process-clubbing.js: -------------------------------------------------------------------------------- 1 | const { Domain } = require('../..') 2 | 3 | process = null 4 | 5 | setTimeout(() => { 6 | const d = new Domain(er => console.log('caught:', er)) 7 | setImmediate(() => { 8 | throw 'thrown' 9 | }) 10 | }) 11 | 12 | setTimeout(() => { 13 | throw new Error('this will not be caught') 14 | }, 100) 15 | -------------------------------------------------------------------------------- /test/fixtures/process-missing.js: -------------------------------------------------------------------------------- 1 | process = null 2 | 3 | const { Domain } = require('../..') 4 | 5 | setTimeout(() => { 6 | const d = new Domain(er => console.log('caught:', er)) 7 | setImmediate(() => { 8 | throw 'thrown' 9 | }) 10 | }) 11 | 12 | setTimeout(() => { 13 | throw new Error('this will not be caught') 14 | }, 100) 15 | -------------------------------------------------------------------------------- /test/fixtures/promise-no-domain.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node --unhandled-rejections=warn 2 | const { Domain } = require('../..') 3 | 4 | Promise.reject('no one to catch this') 5 | 6 | setTimeout(() => { 7 | new Domain(er => console.log('d', er)) 8 | Promise.reject('caught 1') 9 | setTimeout(() => Promise.reject('caught 2'), 250) 10 | }) 11 | 12 | setTimeout(() => { 13 | Promise.reject('happy') 14 | new Promise((_, rej) => rej('sad')) 15 | Promise.resolve('ok').then(ok => { throw ok }) 16 | }, 100) 17 | -------------------------------------------------------------------------------- /test/fixtures/promise-rejections-unhandled-none.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node --unhandled-rejections=none 2 | require('assert').notEqual(process.execArgv.indexOf('--unhandled-rejections=none'), -1) 3 | require('./promise-rejections') 4 | -------------------------------------------------------------------------------- /test/fixtures/promise-rejections-unhandled-strict-with-space.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node --unhandled-rejections=none --unhandled-rejections strict 2 | const {notEqual} = require('assert') 3 | notEqual(process.execArgv.indexOf('--unhandled-rejections=none'), -1) 4 | notEqual(process.execArgv.indexOf('--unhandled-rejections'), -1) 5 | notEqual(process.execArgv.indexOf('strict'), -1) 6 | require('./promise-rejections') 7 | -------------------------------------------------------------------------------- /test/fixtures/promise-rejections-unhandled-strict.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node --unhandled-rejections=strict 2 | require('assert').notEqual(process.execArgv.indexOf('--unhandled-rejections=strict'), -1) 3 | require('./promise-rejections') 4 | -------------------------------------------------------------------------------- /test/fixtures/promise-rejections-unhandled-throw.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node --unhandled-rejections=throw 2 | require('assert').notEqual(process.execArgv.indexOf('--unhandled-rejections=throw'), -1) 3 | require('./promise-rejections') 4 | -------------------------------------------------------------------------------- /test/fixtures/promise-rejections-unhandled-warn-with-error-code.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node --unhandled-rejections=warn-with-error-code 2 | require('assert').notEqual(process.execArgv.indexOf('--unhandled-rejections=warn-with-error-code'), -1) 3 | require('./promise-rejections') 4 | -------------------------------------------------------------------------------- /test/fixtures/promise-rejections-unhandled-warn.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node --unhandled-rejections=warn 2 | require('assert').notEqual(process.execArgv.indexOf('--unhandled-rejections=warn'), -1) 3 | require('./promise-rejections') 4 | -------------------------------------------------------------------------------- /test/fixtures/promise-rejections.js: -------------------------------------------------------------------------------- 1 | const { Domain } = require('../..') 2 | setImmediate(() => { 3 | const d = new Domain((er, where) => { 4 | console.error('CAUGHT', where, er.message) 5 | }) 6 | setImmediate(() => Promise.reject(new Error('happy2'))) 7 | Promise.reject(new Error('happy')) 8 | setTimeout(() => console.log('happy timeout'), 100) 9 | }) 10 | 11 | setTimeout(() => { 12 | // no domain here, this crashes/warns/etc as normal 13 | Promise.reject(new Error('sad')) 14 | setTimeout(() => console.log('sad timeout'), 100) 15 | }, 150) 16 | -------------------------------------------------------------------------------- /test/fixtures/promise-throwing-handler.js: -------------------------------------------------------------------------------- 1 | const { Domain } = require('../..') 2 | const d = new Domain(er => { throw er }) 3 | Promise.reject(new Error('er')) 4 | -------------------------------------------------------------------------------- /test/fixtures/promise-timing.js: -------------------------------------------------------------------------------- 1 | const { Domain } = require('../..') 2 | const d1 = new Domain(er => { console.error('domain 1', er) }, 1) 3 | console.error('call new Promise') 4 | new Promise((_, rej) => { 5 | console.error('in unchained promise') 6 | const d2 = new Domain(er => { console.error('domain 2', er) }, 2) 7 | console.error('schedule timeout to rejection') 8 | setTimeout(() => { 9 | const d3 = new Domain(er => { console.error('domain 3', er) }, 3) 10 | console.error('rejection in timeout, expect caught by domain 1') 11 | rej('rejection should be caught by domain 1') 12 | }) 13 | }) 14 | 15 | const d4 = new Domain(er => { console.error('domain 4', er) }, 4) 16 | Promise.resolve(1234).then(num => { 17 | console.error('in chained promise') 18 | const d5 = new Domain(er => { console.error('domain 5', er) }, 5) 19 | console.error('throw num') 20 | throw `${num} expect caught by domain 5` 21 | }) 22 | 23 | console.error('done with scheduling, trigger immediate rejection') 24 | const d6 = new Domain(er => { console.error('domain 6', er) }, 6) 25 | console.error('reject immediately') 26 | Promise.reject('happy - caught by domain 6') 27 | -------------------------------------------------------------------------------- /test/fixtures/promise.js: -------------------------------------------------------------------------------- 1 | const { Domain } = require('../..') 2 | 3 | const d = new Domain((er, type) => console.log('d', er, type)) 4 | 5 | new Promise((_, rej) => 6 | setTimeout(() => rej('rejection'))) 7 | Promise.resolve(1234).then(num => { throw num }) 8 | Promise.reject('happy') 9 | -------------------------------------------------------------------------------- /test/fixtures/throw-in-handler.js: -------------------------------------------------------------------------------- 1 | const { Domain } = require('../..') 2 | const d = new Domain(er => { throw er }) 3 | 4 | setTimeout(() => { 5 | throw new Error('errr') 6 | }) 7 | -------------------------------------------------------------------------------- /test/fixtures/uncaught.js: -------------------------------------------------------------------------------- 1 | const { Domain } = require('../..') 2 | 3 | setTimeout(() => { 4 | const d = new Domain(er => console.log('caught:', er)) 5 | setImmediate(() => { 6 | throw 'thrown' 7 | }) 8 | }) 9 | 10 | setTimeout(() => { 11 | throw new Error('this will not be caught') 12 | }, 100) 13 | -------------------------------------------------------------------------------- /test/run.js: -------------------------------------------------------------------------------- 1 | const t = require('tap') 2 | 3 | const fs = require('fs') 4 | const file = process.argv[2] 5 | 6 | const node = process.execPath 7 | const {execFile} = require('child_process') 8 | const {relative, resolve} = require('path') 9 | 10 | t.cleanSnapshot = o => o 11 | .replace(/(\n at [^\n]*)+/g, '\n{STACK}') 12 | .split(process.cwd()).join('{CWD}') 13 | .replace(/\.js:[0-9]+(?::[0-9]+)?/g, '.js:#') 14 | .replace(/\.ts:[0-9]+(?::[0-9]+)?/g, '.ts:#') 15 | .replace(/[^\n]*DEP0018[^\n]*\n/g, '') 16 | .replace(/\(node:\d+\)/g, '(node:{PID})') 17 | .replace(/\n+/g, '\n') 18 | .replace(/(\(node:{PID}\) UnhandledPromiseRejectionWarning:).*?(\(rejection id: \d+\)\n)/g, '$1 ... $2') 19 | .replace(/\nNode\.js v?[0-9]+\.[0-9]+\.[0-9]+\n+/g, '') 20 | .split('\n').filter(l => !/node --trace-/.test(l)).join('\n') 21 | // the error callsite is flaky across node versions, and sometimes 22 | // nyc eats it when source-map-support is enabled. just remove. 23 | // tests verify that we got the right error message, that's what's 24 | // typically most important. 25 | .replace(/\{CWD\}[\/\\][^\.]+\.[jt]s:#\s+[^\n]+\n\s*\^\n/g, '') 26 | .trim() + '\n' 27 | 28 | const runTest = file => t => { 29 | const firstLine = fs.readFileSync(file, 'utf8').split(/\n/)[0] 30 | // default all node versions to old default for consistency 31 | const match = firstLine && firstLine.match(/^#!\/usr\/bin\/env node (.*)$/) 32 | || [,''] 33 | 34 | if (!/--unhandled-rejections=/.test(match[1])) { 35 | match[1] += ' --unhandled-rejections=warn-with-error-code' 36 | } 37 | 38 | const args = [ 39 | '--enable-source-maps', 40 | ...match[1].trim().split(' '), 41 | file 42 | ] 43 | t.comment(`node ${args.join(' ')}`) 44 | return execFile(node, args, (er, o, e) => { 45 | t.comment(t.cleanSnapshot(e)) 46 | t.matchSnapshot(er ? {code:er.code, signal: er.signal} : null, 'error') 47 | t.matchSnapshot(o, 'output') 48 | t.matchSnapshot(e, 'stderr') 49 | t.end() 50 | }) 51 | } 52 | 53 | if (file) 54 | runTest(file)(t) 55 | else { 56 | const fixtures = fs.readdirSync(__dirname + '/fixtures') 57 | .filter(f => /\.js$/.test(f)) 58 | .map(f => relative(process.cwd(), __dirname + '/fixtures/' + f)) 59 | t.plan(fixtures.length) 60 | t.jobs = require('os').cpus().length 61 | fixtures.forEach(f => t.test(f, runTest(f))) 62 | } 63 | -------------------------------------------------------------------------------- /tsconfig-base.json: -------------------------------------------------------------------------------- 1 | { 2 | "include": ["./src/*"], 3 | "exclude": ["./test", "./tap-snapshots"], 4 | "compilerOptions": { 5 | "allowSyntheticDefaultImports": true, 6 | "declaration": true, 7 | "declarationMap": true, 8 | "inlineSources": true, 9 | "esModuleInterop": true, 10 | "forceConsistentCasingInFileNames": true, 11 | "moduleResolution": "node16", 12 | "resolveJsonModule": true, 13 | "sourceMap": true, 14 | "strict": true, 15 | "target": "es2022" 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /tsconfig-esm.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig-base.json", 3 | "compilerOptions": { 4 | "module": "esnext", 5 | "outDir": "dist-tmp/mjs" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig-base.json", 3 | "compilerOptions": { 4 | "module": "commonjs", 5 | "outDir": "dist-tmp/cjs" 6 | } 7 | } 8 | --------------------------------------------------------------------------------