├── .editorconfig ├── .github ├── FUNDING.yml └── workflows │ ├── lint.yml │ ├── nodejs-legacy.yml │ ├── nodejs.yml │ ├── ts-internal.yml │ └── ts.yml ├── .gitignore ├── .husky └── pre-push ├── .knip.jsonc ├── .npmrc ├── LICENSE ├── README.md ├── SECURITY.md ├── compat.d.ts ├── declaration.tsconfig.json ├── eslint.config.mjs ├── index.js ├── lib ├── error-with-cause-compat.d.ts ├── error-with-cause.js └── helpers.js ├── logo.svg ├── package.json ├── renovate.json ├── test-published-types ├── .npmrc ├── index.js ├── index.mjs ├── package.json └── tsconfig.json ├── test ├── error.spec.js ├── esm.spec.mjs ├── find.spec.js ├── get.spec.js ├── message.spec.js └── stack.spec.js └── tsconfig.json /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | end_of_line = lf 5 | insert_final_newline = true 6 | indent_style = space 7 | indent_size = 2 8 | charset = utf-8 9 | trim_trailing_whitespace = true 10 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: voxpelli 4 | tidelift: npm/pony-cause 5 | -------------------------------------------------------------------------------- /.github/workflows/lint.yml: -------------------------------------------------------------------------------- 1 | name: Linting 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | tags: 8 | - '*' 9 | pull_request: 10 | branches: 11 | - main 12 | 13 | permissions: 14 | contents: read 15 | 16 | jobs: 17 | lint: 18 | uses: voxpelli/ghatemplates/.github/workflows/lint.yml@main 19 | -------------------------------------------------------------------------------- /.github/workflows/nodejs-legacy.yml: -------------------------------------------------------------------------------- 1 | name: Node CI Legacy 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | tags: 8 | - '*' 9 | pull_request: 10 | branches: 11 | - main 12 | 13 | permissions: 14 | contents: read 15 | 16 | jobs: 17 | test: 18 | uses: voxpelli/ghatemplates/.github/workflows/test.yml@main 19 | with: 20 | node-versions: '12,14' 21 | npm-test-script: 'test-build-less' 22 | npm-no-prepare: true 23 | -------------------------------------------------------------------------------- /.github/workflows/nodejs.yml: -------------------------------------------------------------------------------- 1 | name: Node CI 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | tags: 8 | - '*' 9 | pull_request: 10 | branches: 11 | - main 12 | 13 | permissions: 14 | contents: read 15 | 16 | jobs: 17 | test: 18 | uses: voxpelli/ghatemplates/.github/workflows/test.yml@main 19 | with: 20 | node-versions: '16,18,20,21' 21 | -------------------------------------------------------------------------------- /.github/workflows/ts-internal.yml: -------------------------------------------------------------------------------- 1 | name: Type Checks, Internal Types 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | tags: 8 | - '*' 9 | pull_request: 10 | branches: 11 | - main 12 | schedule: 13 | - cron: '14 5 * * 1,3,5' 14 | 15 | permissions: 16 | contents: read 17 | 18 | jobs: 19 | type-check: 20 | uses: voxpelli/ghatemplates/.github/workflows/type-check.yml@main 21 | with: 22 | ts-versions: ${{ github.event.schedule && 'next' || '5.0,next' }} 23 | ts-libs: 'es2020' 24 | -------------------------------------------------------------------------------- /.github/workflows/ts.yml: -------------------------------------------------------------------------------- 1 | name: Type Checks, Published Types 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | tags: 8 | - '*' 9 | pull_request: 10 | branches: 11 | - main 12 | schedule: 13 | - cron: '14 5 * * 1,3,5' 14 | 15 | permissions: 16 | contents: read 17 | 18 | jobs: 19 | type-check: 20 | uses: voxpelli/ghatemplates/.github/workflows/type-check.yml@main 21 | with: 22 | ts-prebuild-script: 'build' 23 | ts-versions: ${{ github.event.schedule && 'next' || '4.5,4.6,4.7,4.8,4.9,5.0,next' }} 24 | # Can add the "es2020,es2022.error,es2021.promise" once 4.5 isn't included 25 | # ts-libs: 'es2020;esnext;es2020,es2022.error,es2021.promise' 26 | ts-libs: 'es2020;esnext' 27 | ts-working-directory: 'test-published-types' 28 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Basic ones 2 | /coverage 3 | /docs 4 | /node_modules 5 | /.env 6 | /.nyc_output 7 | 8 | # We're a library, so please, no lock files 9 | /package-lock.json 10 | /yarn.lock 11 | 12 | # Generated types 13 | *.d.ts 14 | *.d.ts.map 15 | 16 | # Library specific ones 17 | /lib/**/*.mjs 18 | /index.mjs 19 | !*compat.d.ts 20 | /test-published-types/node_modules 21 | -------------------------------------------------------------------------------- /.husky/pre-push: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | 4 | npm test 5 | -------------------------------------------------------------------------------- /.knip.jsonc: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://unpkg.com/knip@5/schema.json", 3 | "entry": [ 4 | "compat.d.ts", 5 | "index.js", 6 | "index.mjs", 7 | "index.d.ts" 8 | ], 9 | "ignore": ["test-published-types/*"] 10 | } 11 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | package-lock=false 2 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | BSD Zero Clause License (0BSD) 2 | 3 | Copyright (c) 2020 Pelle Wessman 4 | 5 | Permission to use, copy, modify, and/or distribute this software for any 6 | purpose with or without fee is hereby granted. 7 | 8 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH 9 | REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY 10 | AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, 11 | INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM 12 | LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR 13 | OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR 14 | PERFORMANCE OF THIS SOFTWARE. 15 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 | pony-cause 8 |
9 | 10 |
11 | 12 | Helpers and [ponyfill](https://ponyfill.com/) for [Error Causes](https://github.com/tc39/proposal-error-cause) 13 | 14 | [![npm version](https://img.shields.io/npm/v/pony-cause.svg?style=flat)](https://www.npmjs.com/package/pony-cause) 15 | [![npm downloads](https://img.shields.io/npm/dm/pony-cause.svg?style=flat)](https://www.npmjs.com/package/pony-cause) 16 | [![Module type: CJS+ESM](https://img.shields.io/badge/module%20type-cjs%2Besm-brightgreen)](https://github.com/voxpelli/badges-cjs-esm) 17 | [![Types in JS](https://img.shields.io/badge/types_in_js-yes-brightgreen)](https://github.com/voxpelli/types-in-js) 18 | [![neostandard javascript style](https://img.shields.io/badge/code_style-neostandard-7fffff?style=flat&labelColor=ff80ff)](https://github.com/neostandard/neostandard) 19 | [![Follow @voxpelli@mastodon.social](https://img.shields.io/mastodon/follow/109247025527949675?domain=https%3A%2F%2Fmastodon.social&style=social)](https://mastodon.social/@voxpelli) 20 | 21 |
22 | 23 | ## Exports 24 | 25 | ### Helpers for working with error causes 26 | 27 | * [`findCauseByReference`](#findcausebyreference) - finding an error of a specific type within the cause chain 28 | * [`getErrorCause`](#geterrorcause) - getting the direct cause of an error, if there is any 29 | * [`messageWithCauses`](#messagewithcauses) - gets the error message with the messages of its cause chain appended to it 30 | * [`stackWithCauses`](#stackwithcauses) - gets a stack trace for the error + all its causes 31 | 32 | All the above are backwards compatible with causes created by the [`VError`](https://github.com/TritonDataCenter/node-verror) module which predated the Error Causes spec and is still used in parts of the ecosystem. 33 | 34 | ### Ponyfill for Error Causes 35 | 36 | * [`ErrorWithCause`](#errorwithcause) - an exported `Error` subclass that works like the [Error Causes](https://github.com/tc39/proposal-error-cause) spec. By using this class you ["ponyfill"](https://ponyfill.com/) the spec locally rather than eg. polyfilling it globally. 37 | 38 | ## CJS + ESM + Types 39 | 40 | `pony-cause` is dual published as both CommonJS and ESM, use whichever you like and make use of the TypeScript compliant types no matter which. 41 | 42 | ## Examples 43 | 44 | ### `ErrorWithCause` 45 | 46 | [Ponyfill](https://ponyfill.com/) of the `cause`-supporting `Error` class 47 | 48 | ```javascript 49 | const { ErrorWithCause } = require('pony-cause'); 50 | 51 | try { /* Something that can break */ } catch (err) { 52 | throw new ErrorWithCause('Failed doing what I intended', { cause: err }); 53 | } 54 | ``` 55 | 56 | ### `findCauseByReference` 57 | 58 | Finding an error of a specific type within the cause chain. Is typescript friendly. 59 | 60 | ```javascript 61 | const { findCauseByReference } = require('pony-cause'); 62 | 63 | try { /* Something that can break */ } catch (err) { 64 | /** @type {MySpecialError} */ 65 | const specialErr = findCauseByReference(err, MySpecialError); 66 | 67 | if (specialErr && specialErr.specialProperty === 'specialValue') { 68 | // Its okay, chill! 69 | } else { 70 | throw err; 71 | } 72 | } 73 | ``` 74 | 75 | Used to find a specific type of error in the chain of causes in an error. 76 | 77 | Similar to [`VError.findCauseByName`](https://github.com/TritonDataCenter/node-verror#verrorfindcausebynameerr-name) but resolves causes in both [Error Causes](https://github.com/tc39/proposal-error-cause) style, `.cause`, and [VError](https://github.com/TritonDataCenter/node-verror) style, `.cause()` + takes a reference to the Error class that you are looking for rather than simply the name of it, as that enables the TypeScript types to properly type the returned error, typing it with the same type as the reference. 78 | 79 | Can be useful if there's some extra data on it that can help determine whether it's an unexpected error or an error that can be handled. 80 | 81 | If it's an error related to a HTTP request, then maybe the request can be retried? If its a database error that tells you about a duplicated row, then maybe you know how to work with that? Maybe forward that error to the user rather than show a `500` error? 82 | 83 | _Note:_ [`findCauseByReference`](#findcausebyreference) has protection against circular causes 84 | 85 | ### `getErrorCause` 86 | 87 | Getting the direct cause of an error, if there is any 88 | 89 | ```javascript 90 | const { getErrorCause } = require('pony-cause'); 91 | 92 | try { /* Something that can break */ } catch (err) { 93 | // Returns the Error cause, VError cause or undefined 94 | const cause = getErrorCause(err); 95 | } 96 | ``` 97 | 98 | The output is similar to [`VError.cause()`](https://github.com/TritonDataCenter/node-verror#verrorcauseerr) but resolves causes in both [Error Causes](https://github.com/tc39/proposal-error-cause) style, `.cause`, and [VError](https://github.com/TritonDataCenter/node-verror) style, `.cause()`. 99 | 100 | Always return an `Error`, a subclass of `Error` or `undefined`. If a cause in [Error Causes](https://github.com/tc39/proposal-error-cause) style cause is not an `Error` or a subclass of `Error`, it will be ignored and `undefined` will be returned. 101 | 102 | ### `messageWithCauses` 103 | 104 | Gets the error message with the messages of its cause chain appended to it. 105 | 106 | ```javascript 107 | const { messageWithCauses, ErrorWithCause } = require('pony-cause'); 108 | 109 | try { 110 | try { 111 | // First error... 112 | throw new Error('First error'); 113 | } catch (err) { 114 | // ...that's caught and wrapped in a second error 115 | throw new ErrorWithCause('Second error', { cause: err }); 116 | } 117 | } catch (err) { 118 | // Logs the full message trail: "Second error: First error" 119 | console.log(messageWithCauses(err)); 120 | } 121 | ``` 122 | 123 | The output is similar to the standard `VError` behaviour of [appending `message` with the `cause.message`](https://github.com/TritonDataCenter/node-verror#public-properties), separating the two with a `: `. 124 | 125 | Since [Error Causes](https://github.com/tc39/proposal-error-cause) doesn't do this, [`messageWithCauses`](#messagewithcauses) exist to mimic that behaviour. 126 | 127 | It respects `VError` messages, it won't append any error message of their causes, though it will walk past the `VError` causes to see if there's a non-VError cause up the chain and then append that. 128 | 129 | The reason to use this method is explained by `VError`: 130 | 131 | > The idea is that each layer in the stack annotates the error with a description of what it was doing. The end result is a message that explains what happened at each level. 132 | 133 | If an inner error has a message `ENOENT, stat '/nonexistent'`, an outer error wraps it and adds `Can't perform X` and maybe one more error wraps that and adds `Can't start program`, then [`messageWithCauses`](#messagewithcauses) will join those three errors together when providing it with the outer most error and return `Can't start program: Can't perform X: ENOENT, stat '/nonexistent'` which provides details about both cause and effect as well as the connection between the two – each which on their own would be a lot harder to understand the impact of. 134 | 135 | _Note:_ [`messageWithCauses`](#messagewithcauses) has protection against circular causes 136 | 137 | ### `stackWithCauses` 138 | 139 | Gets a stack trace for the error + all its causes. 140 | 141 | ```javascript 142 | const { stackWithCauses } = require('pony-cause'); 143 | 144 | try { /* Something that can break */ } catch (err) { 145 | console.log('We had a mishap:', stackWithCauses(err)); 146 | } 147 | ``` 148 | 149 | The output is similar to [`VError.fullStack()`](https://github.com/TritonDataCenter/node-verror#verrorfullstackerr) but resolves causes in both [Error Causes](https://github.com/tc39/proposal-error-cause) style, `.cause`, and [VError](https://github.com/TritonDataCenter/node-verror) style, `.cause()`. 150 | 151 | _Note:_ [`stackWithCauses`](#stackwithcauses) has protection against circular causes 152 | 153 | Output looks like: 154 | 155 | ``` 156 | Error: something really bad happened here 157 | at Object. (/examples/fullStack.js:5:12) 158 | at Module._compile (module.js:409:26) 159 | at Object.Module._extensions..js (module.js:416:10) 160 | at Module.load (module.js:343:32) 161 | at Function.Module._load (module.js:300:12) 162 | at Function.Module.runMain (module.js:441:10) 163 | at startup (node.js:139:18) 164 | at node.js:968:3 165 | caused by: Error: something bad happened 166 | at Object. (/examples/fullStack.js:3:12) 167 | at Module._compile (module.js:409:26) 168 | at Object.Module._extensions..js (module.js:416:10) 169 | at Module.load (module.js:343:32) 170 | at Function.Module._load (module.js:300:12) 171 | at Function.Module.runMain (module.js:441:10) 172 | at startup (node.js:139:18) 173 | at node.js:968:3 174 | ``` 175 | 176 | ## For enterprise 177 | 178 | Available as part of the Tidelift Subscription. 179 | 180 | The maintainers of `pony-cause` and thousands of other packages are working with Tidelift to deliver commercial support and maintenance for the open source packages you use to build your applications. Save time, reduce risk, and improve code health, while paying the maintainers of the exact packages you use. [Learn more.](https://tidelift.com/subscription/pkg/npm-pony-cause?utm_source=npm-pony-cause&utm_medium=referral&utm_campaign=enterprise) 181 | 182 | ## Similar modules 183 | 184 | * [`verror`](https://www.npmjs.com/package/verror) – a module which has long enabled error causes in javascript. Superseded by the new Error Cause proposal. Differs in that`.cause` represents a function that returns the cause, its not the cause itself. 185 | * [`@netflix/nerror`](https://www.npmjs.com/package/@netflix/nerror) – a Netflix fork of `verror` 186 | * [`error-cause`](https://www.npmjs.com/package/error-cause) – strict polyfill for the Error Cause proposal. Provides no helpers or similar to achieve `VError`-like functionality, which `pony-cause` does. 187 | 188 | ## See also 189 | 190 | * [Pony Cause announcement blog post](https://dev.to/voxpelli/pony-cause-1-0-error-causes-2l2o) 191 | * [Pony Cause announcement tweet](https://twitter.com/voxpelli/status/1438476680537034756) 192 | * [Error Cause implementations](https://github.com/tc39/proposal-error-cause#implementations) 193 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | ## Supported Versions 4 | 5 | The latest minor release, unless stated otherwise 6 | 7 | ## Reporting a Vulnerability 8 | 9 | To report a security vulnerability, please use the 10 | [Tidelift security contact](https://tidelift.com/security). 11 | Tidelift will coordinate the fix and disclosure. 12 | -------------------------------------------------------------------------------- /compat.d.ts: -------------------------------------------------------------------------------- 1 | export { ErrorWithCause } from './lib/error-with-cause-compat'; 2 | export * from './lib/helpers'; 3 | -------------------------------------------------------------------------------- /declaration.tsconfig.json: -------------------------------------------------------------------------------- 1 | 2 | { 3 | "extends": "./tsconfig", 4 | "exclude": [ 5 | "test/**/*.js", 6 | "test-published-types/**/*" 7 | ], 8 | "compilerOptions": { 9 | "declaration": true, 10 | "declarationMap": true, 11 | "noEmit": false, 12 | "emitDeclarationOnly": true, 13 | "removeComments": true 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /eslint.config.mjs: -------------------------------------------------------------------------------- 1 | import { voxpelli } from '@voxpelli/eslint-config'; 2 | 3 | export default voxpelli({ cjs: true }); 4 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const { ErrorWithCause } = require('./lib/error-with-cause'); // linemod-replace-with: export { ErrorWithCause } from './lib/error-with-cause.mjs'; 4 | 5 | const { // linemod-replace-with: export { 6 | findCauseByReference, 7 | getErrorCause, 8 | messageWithCauses, 9 | stackWithCauses, 10 | } = require('./lib/helpers'); // linemod-replace-with: } from './lib/helpers.mjs'; 11 | 12 | module.exports = { // linemod-remove 13 | ErrorWithCause, // linemod-remove 14 | findCauseByReference, // linemod-remove 15 | getErrorCause, // linemod-remove 16 | stackWithCauses, // linemod-remove 17 | messageWithCauses, // linemod-remove 18 | }; // linemod-remove 19 | -------------------------------------------------------------------------------- /lib/error-with-cause-compat.d.ts: -------------------------------------------------------------------------------- 1 | export class ErrorWithCause extends Error { 2 | constructor (message: string, { cause }?: { 3 | cause?: unknown; 4 | } | undefined); 5 | // We need to be stricter here because of esnext lib in TS 4.6 and TS 4.7 6 | cause?: Error; 7 | } 8 | -------------------------------------------------------------------------------- /lib/error-with-cause.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** @template [T=undefined] */ 4 | class ErrorWithCause extends Error { // linemod-prefix-with: export 5 | /** 6 | * @param {string} message 7 | * @param {{ cause?: T }} options 8 | */ 9 | constructor (message, { cause } = {}) { 10 | super(message); 11 | 12 | /** @type {string} */ 13 | this.name = ErrorWithCause.name; 14 | if (cause) { 15 | /** @type {T} */ 16 | this.cause = cause; 17 | } 18 | /** @type {string} */ 19 | this.message = message; 20 | } 21 | } 22 | 23 | module.exports = { // linemod-remove 24 | ErrorWithCause, // linemod-remove 25 | }; // linemod-remove 26 | -------------------------------------------------------------------------------- /lib/helpers.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * @template {Error} T 5 | * @param {unknown} err 6 | * @param {new(...args: any[]) => T} reference 7 | * @returns {T|undefined} 8 | */ 9 | const findCauseByReference = (err, reference) => { // linemod-prefix-with: export 10 | if (!err || !reference) return; 11 | if (!(err instanceof Error)) return; 12 | if ( 13 | !(reference.prototype instanceof Error) && 14 | // @ts-ignore 15 | reference !== Error 16 | ) return; 17 | 18 | /** 19 | * Ensures we don't go circular 20 | * 21 | * @type {Set} 22 | */ 23 | const seen = new Set(); 24 | 25 | /** @type {Error|undefined} */ 26 | let currentErr = err; 27 | 28 | while (currentErr && !seen.has(currentErr)) { 29 | seen.add(currentErr); 30 | 31 | if (currentErr instanceof reference) { 32 | return currentErr; 33 | } 34 | 35 | currentErr = getErrorCause(currentErr); 36 | } 37 | }; 38 | 39 | /** 40 | * @param {Error|{ cause?: unknown|(()=>err)}} err 41 | * @returns {Error|undefined} 42 | */ 43 | const getErrorCause = (err) => { // linemod-prefix-with: export 44 | if (!err || typeof err !== 'object' || !('cause' in err)) { 45 | return; 46 | } 47 | 48 | // VError / NError style causes 49 | if (typeof err.cause === 'function') { 50 | const causeResult = err.cause(); 51 | 52 | return causeResult instanceof Error 53 | ? causeResult 54 | : undefined; 55 | } else { 56 | return err.cause instanceof Error 57 | ? err.cause 58 | : undefined; 59 | } 60 | }; 61 | 62 | /** 63 | * Internal method that keeps a track of which error we have already added, to avoid circular recursion 64 | * 65 | * @private 66 | * @param {Error} err 67 | * @param {Set} seen 68 | * @returns {string} 69 | */ 70 | const _stackWithCauses = (err, seen) => { 71 | if (!(err instanceof Error)) return ''; 72 | 73 | const stack = err.stack || ''; 74 | 75 | // Ensure we don't go circular or crazily deep 76 | if (seen.has(err)) { 77 | return stack + '\ncauses have become circular...'; 78 | } 79 | 80 | const cause = getErrorCause(err); 81 | 82 | // TODO: Follow up in https://github.com/nodejs/node/issues/38725#issuecomment-920309092 on how to log stuff 83 | 84 | if (cause) { 85 | seen.add(err); 86 | return (stack + '\ncaused by: ' + _stackWithCauses(cause, seen)); 87 | } else { 88 | return stack; 89 | } 90 | }; 91 | 92 | /** 93 | * @param {Error} err 94 | * @returns {string} 95 | */ 96 | const stackWithCauses = (err) => _stackWithCauses(err, new Set()); // linemod-prefix-with: export 97 | 98 | /** 99 | * Internal method that keeps a track of which error we have already added, to avoid circular recursion 100 | * 101 | * @private 102 | * @param {Error} err 103 | * @param {Set} seen 104 | * @param {boolean} [skip] 105 | * @returns {string} 106 | */ 107 | const _messageWithCauses = (err, seen, skip) => { 108 | if (!(err instanceof Error)) return ''; 109 | 110 | const message = skip ? '' : (err.message || ''); 111 | 112 | // Ensure we don't go circular or crazily deep 113 | if (seen.has(err)) { 114 | return message + ': ...'; 115 | } 116 | 117 | const cause = getErrorCause(err); 118 | 119 | if (cause) { 120 | seen.add(err); 121 | 122 | const skipIfVErrorStyleCause = 'cause' in err && typeof err.cause === 'function'; 123 | 124 | return (message + 125 | (skipIfVErrorStyleCause ? '' : ': ') + 126 | _messageWithCauses(cause, seen, skipIfVErrorStyleCause)); 127 | } else { 128 | return message; 129 | } 130 | }; 131 | 132 | /** 133 | * @param {Error} err 134 | * @returns {string} 135 | */ 136 | const messageWithCauses = (err) => _messageWithCauses(err, new Set()); // linemod-prefix-with: export 137 | 138 | module.exports = { // linemod-remove 139 | findCauseByReference, // linemod-remove 140 | getErrorCause, // linemod-remove 141 | stackWithCauses, // linemod-remove 142 | messageWithCauses, // linemod-remove 143 | }; // linemod-remove 144 | -------------------------------------------------------------------------------- /logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | pony-cause 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "pony-cause", 3 | "version": "2.1.11", 4 | "description": "Ponyfill and helpers for Error Causes", 5 | "homepage": "http://github.com/voxpelli/pony-cause", 6 | "repository": { 7 | "type": "git", 8 | "url": "git://github.com/voxpelli/pony-cause.git" 9 | }, 10 | "main": "index.js", 11 | "module": "index.mjs", 12 | "types": "index.d.ts", 13 | "typesVersions": { 14 | "~4.6 || ~4.7": { 15 | "index.d.ts": [ 16 | "compat.d.ts" 17 | ] 18 | } 19 | }, 20 | "exports": { 21 | ".": { 22 | "types@~4.6": "./compat.d.ts", 23 | "types@~4.7": "./compat.d.ts", 24 | "types": "./index.d.ts", 25 | "import": "./index.mjs", 26 | "require": "./index.js" 27 | } 28 | }, 29 | "files": [ 30 | "/compat.d.ts", 31 | "/index.js", 32 | "/index.mjs", 33 | "/index.d.ts", 34 | "/index.d.ts.map", 35 | "lib/**/*.js", 36 | "lib/**/*.mjs", 37 | "lib/**/*.d.ts", 38 | "lib/**/*.d.ts.map" 39 | ], 40 | "scripts": { 41 | "build-for-test": "run-s clean build:1:esm", 42 | "build:0": "run-s clean", 43 | "build:1:declaration": "tsc -p declaration.tsconfig.json", 44 | "build:1:esm": "linemod -e mjs index.js lib/*.js", 45 | "build:1": "run-p build:1:*", 46 | "build": "run-s build:*", 47 | "check:0": "run-s build-for-test", 48 | "check:1:installed-check": "installed-check --ignore-dev", 49 | "check:1:knip": "knip", 50 | "check:1:lint": "eslint --report-unused-disable-directives .", 51 | "check:1:tsc": "tsc", 52 | "check:1:type-coverage": "type-coverage --detail --strict --at-least 97 --ignore-files 'test/*'", 53 | "check:1": "run-p -c --aggregate-output check:1:*", 54 | "check": "run-s check:*", 55 | "clean:declarations": "rm -rf $(find . -maxdepth 2 -type f -name '*.d.ts*' ! -name '*compat.d.ts')", 56 | "clean": "run-p clean:*", 57 | "prepare": "husky install > /dev/null", 58 | "prepublishOnly": "run-s build", 59 | "test:0": "run-s build-for-test", 60 | "test:1-mocha": "c8 --reporter=lcov --reporter text mocha 'test/**/*.spec.js' 'test/**/*.spec.mjs'", 61 | "test-build-less": "mocha 'test/**/*.spec.js'", 62 | "test-ci": "run-s test:*", 63 | "test": "run-s check test:*" 64 | }, 65 | "keywords": [ 66 | "ponyfill", 67 | "error", 68 | "error-cause" 69 | ], 70 | "author": "Pelle Wessman (http://kodfabrik.se/)", 71 | "license": "0BSD", 72 | "engines": { 73 | "node": ">=12.0.0" 74 | }, 75 | "devDependencies": { 76 | "@types/chai": "^4.3.16", 77 | "@types/chai-string": "^1.4.5", 78 | "@types/mocha": "^10.0.7", 79 | "@types/node": "^18.19.42", 80 | "@types/verror": "^1.10.10", 81 | "@voxpelli/eslint-config": "^22.1.0", 82 | "@voxpelli/tsconfig": "^13.0.0", 83 | "c8": "^10.1.2", 84 | "chai": "^4.4.1", 85 | "chai-string": "^1.5.0", 86 | "eslint": "^9.7.0", 87 | "husky": "^9.1.1", 88 | "installed-check": "^9.3.0", 89 | "knip": "^5.27.0", 90 | "linemod": "^2.0.1", 91 | "mocha": "^10.7.0", 92 | "npm-run-all2": "^6.2.2", 93 | "type-coverage": "^2.29.1", 94 | "typescript": "~5.5.3", 95 | "verror": "^1.10.1" 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "github>voxpelli/renovate-config" 4 | ] 5 | } 6 | -------------------------------------------------------------------------------- /test-published-types/.npmrc: -------------------------------------------------------------------------------- 1 | package-lock=false 2 | -------------------------------------------------------------------------------- /test-published-types/index.js: -------------------------------------------------------------------------------- 1 | const { ErrorWithCause } = require('pony-cause'); 2 | 3 | throw new ErrorWithCause('Wow', { cause: new Error('Yay') }); 4 | -------------------------------------------------------------------------------- /test-published-types/index.mjs: -------------------------------------------------------------------------------- 1 | import { ErrorWithCause } from 'pony-cause'; 2 | 3 | throw new ErrorWithCause('Wow', { cause: new Error('Yay') }); 4 | -------------------------------------------------------------------------------- /test-published-types/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@voxpelli/test-published-types", 3 | "private": true, 4 | "version": "0.0.0", 5 | "dependencies": { 6 | "pony-cause": "file:.." 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /test-published-types/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@voxpelli/tsconfig/legacy.json", 3 | 4 | "files": [ 5 | "index.js", 6 | "index.mjs" 7 | ], 8 | 9 | "compilerOptions": { 10 | "lib": ["esnext"], 11 | "types": [] 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /test/error.spec.js: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | /// 4 | 5 | 'use strict'; 6 | 7 | const chai = require('chai'); 8 | 9 | chai.use(require('chai-string')); 10 | chai.should(); 11 | 12 | const { 13 | ErrorWithCause, 14 | } = require('..'); 15 | 16 | describe('ErrorWithCause', () => { 17 | it('should set cause when provided', () => { 18 | const cause = new Error('Bar'); 19 | const err = new ErrorWithCause('Foo', { cause }); 20 | 21 | err.should.have.property('cause', cause); 22 | err.should.have.property('message', 'Foo'); 23 | }); 24 | 25 | it('should handle missing options object', () => { 26 | const err = new ErrorWithCause('Foo'); 27 | 28 | err.should.not.have.property('cause'); 29 | err.should.have.property('message', 'Foo'); 30 | }); 31 | 32 | it('should handle empty options object', () => { 33 | (new ErrorWithCause('Foo', {})).should.not.have.property('cause'); 34 | }); 35 | 36 | it('should produce a proper stack trace', () => { 37 | const err = new ErrorWithCause('Foo'); 38 | err.should.have.property('stack').that.is.a('string').which.startsWith('ErrorWithCause: Foo\n'); 39 | }); 40 | }); 41 | -------------------------------------------------------------------------------- /test/esm.spec.mjs: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | /// 4 | 5 | 'use strict'; 6 | 7 | import chai from 'chai'; 8 | import { ErrorWithCause } from '../index.mjs'; 9 | 10 | chai.should(); 11 | 12 | describe('ESM ErrorWithCause', () => { 13 | it('should set cause when provided', () => { 14 | const cause = new Error('Bar'); 15 | const err = new ErrorWithCause('Foo', { cause }); 16 | 17 | err.should.have.property('cause', cause); 18 | err.should.have.property('message', 'Foo'); 19 | }); 20 | }); 21 | -------------------------------------------------------------------------------- /test/find.spec.js: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | /// 4 | 5 | 'use strict'; 6 | 7 | const chai = require('chai'); 8 | 9 | const should = chai.should(); 10 | 11 | const VError = require('verror'); 12 | 13 | const { 14 | ErrorWithCause, 15 | findCauseByReference, 16 | } = require('..'); 17 | 18 | class SubError extends Error {} 19 | 20 | describe('findCauseByReference()', () => { 21 | describe('should have a resilient API which', () => { 22 | it('should handle being given nothing', () => { 23 | // @ts-ignore 24 | const result = findCauseByReference(); 25 | should.not.exist(result); 26 | }); 27 | 28 | it('should handle being given only an error', () => { 29 | // @ts-ignore 30 | const result = findCauseByReference(new Error('Yay')); 31 | should.not.exist(result); 32 | }); 33 | 34 | it('should return nothing if given a non-error', () => { 35 | // @ts-ignore 36 | const result = findCauseByReference(true, true); 37 | should.not.exist(result); 38 | }); 39 | 40 | it('should return nothing if given null', () => { 41 | // @ts-ignore 42 | // eslint-disable-next-line unicorn/no-null 43 | const result = findCauseByReference(null, null); 44 | should.not.exist(result); 45 | }); 46 | 47 | it('should return nothing if given a non-error reference', () => { 48 | // @ts-ignore 49 | const result = findCauseByReference(new Error('yay'), true); 50 | should.not.exist(result); 51 | }); 52 | 53 | it('should return nothing if given a non-Error constructor as reference', () => { 54 | class Foo {} 55 | // @ts-ignore 56 | const result = findCauseByReference(new Error('yay'), Foo); 57 | should.not.exist(result); 58 | }); 59 | }); 60 | 61 | it('should return input if its an instance of reference', () => { 62 | const err = new SubError('Foo'); 63 | const result = findCauseByReference(err, SubError); 64 | should.exist(result); 65 | (result || {}).should.equal(err); 66 | }); 67 | 68 | it('should return input if its an instance of a parent of reference', () => { 69 | const err = new SubError('Foo'); 70 | const result = findCauseByReference(err, Error); 71 | should.exist(result); 72 | (result || {}).should.equal(err); 73 | }); 74 | 75 | it('should not return input if its not an instance of reference', () => { 76 | const err = new Error('Foo'); 77 | const result = findCauseByReference(err, SubError); 78 | should.not.exist(result); 79 | }); 80 | 81 | it('should return input cause if its an instance of reference', () => { 82 | const cause = new SubError('Foo'); 83 | const err = new ErrorWithCause('Bar', { cause }); 84 | const result = findCauseByReference(err, SubError); 85 | should.exist(result); 86 | (result || {}).should.equal(cause); 87 | }); 88 | 89 | it('should return input VError cause if its an instance of reference', () => { 90 | const cause = new SubError('Foo'); 91 | const err = new VError(cause, 'Bar'); 92 | const result = findCauseByReference(err, SubError); 93 | should.exist(result); 94 | (result || {}).should.equal(cause); 95 | }); 96 | 97 | it('should not go infinite on circular error causes', () => { 98 | const cause = new ErrorWithCause('Foo'); 99 | const err = new ErrorWithCause('Bar', { cause }); 100 | 101 | // @ts-ignore 102 | cause.cause = err; 103 | 104 | const result = findCauseByReference(err, SubError); 105 | should.not.exist(result); 106 | }); 107 | }); 108 | -------------------------------------------------------------------------------- /test/get.spec.js: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | /// 4 | 5 | 'use strict'; 6 | 7 | const chai = require('chai'); 8 | 9 | const should = chai.should(); 10 | 11 | const VError = require('verror'); 12 | 13 | const { 14 | ErrorWithCause, 15 | getErrorCause, 16 | } = require('..'); 17 | 18 | class SubError extends Error {} 19 | 20 | describe('getErrorCause()', () => { 21 | describe('should have a resilient API which', () => { 22 | it('should handle being given nothing', () => { 23 | // @ts-ignore 24 | const result = getErrorCause(); 25 | should.not.exist(result); 26 | }); 27 | 28 | it('should return nothing if given a non-error', () => { 29 | // @ts-ignore 30 | const result = getErrorCause(true); 31 | should.not.exist(result); 32 | }); 33 | 34 | it('should return nothing if given null', () => { 35 | // @ts-ignore 36 | // eslint-disable-next-line unicorn/no-null 37 | const result = getErrorCause(null); 38 | should.not.exist(result); 39 | }); 40 | 41 | it('should return nothing if given a non-cause Error', () => { 42 | const result = getErrorCause(new Error('Foo')); 43 | should.not.exist(result); 44 | }); 45 | }); 46 | 47 | describe('with Error Cause input', () => { 48 | it('should return cause', () => { 49 | const cause = new SubError('Foo'); 50 | const err = new ErrorWithCause('Bar', { cause }); 51 | const result = getErrorCause(err); 52 | should.exist(result); 53 | (result || {}).should.equal(cause); 54 | }); 55 | 56 | it('should not return non-Error cause', () => { 57 | const err = new ErrorWithCause('Bar', { 58 | // @ts-ignore Can be removed when we no longer support TS 4.7 59 | cause: '123', 60 | }); 61 | const result = getErrorCause(err); 62 | should.not.exist(result); 63 | }); 64 | }); 65 | 66 | describe('with VError compatibility', () => { 67 | it('should return cause', () => { 68 | const cause = new SubError('Foo'); 69 | const err = new VError(cause, 'Bar'); 70 | const result = getErrorCause(err); 71 | should.exist(result); 72 | (result || {}).should.equal(cause); 73 | }); 74 | 75 | it('should not return non-Error cause', () => { 76 | const err = { cause () { return '123'; } }; 77 | const result = getErrorCause(err); 78 | should.not.exist(result); 79 | }); 80 | }); 81 | }); 82 | -------------------------------------------------------------------------------- /test/message.spec.js: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | /// 4 | 5 | 'use strict'; 6 | 7 | const chai = require('chai'); 8 | 9 | const should = chai.should(); 10 | 11 | const VError = require('verror'); 12 | 13 | const { 14 | ErrorWithCause, 15 | messageWithCauses, 16 | } = require('..'); 17 | 18 | describe('messageWithCauses()', () => { 19 | describe('should have a resilient API which', () => { 20 | it('should handle being given nothing', () => { 21 | // @ts-ignore 22 | const result = messageWithCauses(); 23 | should.exist(result); 24 | result.should.equal(''); 25 | }); 26 | 27 | it('should handle being given null', () => { 28 | // @ts-ignore 29 | // eslint-disable-next-line unicorn/no-null 30 | const result = messageWithCauses(null); 31 | should.exist(result); 32 | result.should.equal(''); 33 | }); 34 | 35 | it('should handle an undefined message attribute', () => { 36 | const err = new Error('foo'); 37 | // @ts-ignore 38 | err.message = undefined; 39 | 40 | const result = messageWithCauses(err); 41 | should.exist(result); 42 | result.should.equal(''); 43 | }); 44 | }); 45 | 46 | it('should return the message', () => { 47 | const err = new Error('Foo'); 48 | const result = messageWithCauses(err); 49 | should.exist(result); 50 | result.should.equal('Foo'); 51 | }); 52 | 53 | it('should append causes to the message', () => { 54 | const cause = new Error('Foo'); 55 | const err = new ErrorWithCause('Bar', { cause }); 56 | const result = messageWithCauses(err); 57 | should.exist(result); 58 | result.should.equal('Bar: Foo'); 59 | }); 60 | 61 | it('should append VError causes to the message', () => { 62 | const cause1 = new Error('Foo'); 63 | const cause2 = new ErrorWithCause('Abc', { cause: cause1 }); 64 | const cause3 = new VError(cause2, 'Bar'); 65 | const err = new ErrorWithCause('Xyz', { cause: cause3 }); 66 | 67 | const result = messageWithCauses(err); 68 | should.exist(result); 69 | result.should.equal('Xyz: Bar: Abc: Foo'); 70 | }); 71 | 72 | it('should not go infinite on circular error causes', () => { 73 | const cause = new ErrorWithCause('Foo'); 74 | const err = new ErrorWithCause('Bar', { cause }); 75 | 76 | // @ts-ignore 77 | cause.cause = err; 78 | 79 | const result = messageWithCauses(err); 80 | result.should.equal('Bar: Foo: Bar: ...'); 81 | }); 82 | }); 83 | -------------------------------------------------------------------------------- /test/stack.spec.js: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | /// 4 | 5 | 'use strict'; 6 | 7 | const chai = require('chai'); 8 | 9 | const should = chai.should(); 10 | 11 | const VError = require('verror'); 12 | 13 | const { 14 | ErrorWithCause, 15 | stackWithCauses, 16 | } = require('..'); 17 | 18 | describe('stackWithCauses()', () => { 19 | describe('should have a resilient API which', () => { 20 | it('should handle being given nothing', () => { 21 | // @ts-ignore 22 | const result = stackWithCauses(); 23 | should.exist(result); 24 | result.should.equal(''); 25 | }); 26 | 27 | it('should handle being given null', () => { 28 | // @ts-ignore 29 | // eslint-disable-next-line unicorn/no-null 30 | const result = stackWithCauses(null); 31 | should.exist(result); 32 | result.should.equal(''); 33 | }); 34 | 35 | it('should handle an undefined stack attribute', () => { 36 | const err = new Error('foo'); 37 | // @ts-ignore 38 | err.stack = undefined; 39 | 40 | const result = stackWithCauses(err); 41 | should.exist(result); 42 | result.should.equal(''); 43 | }); 44 | }); 45 | 46 | it('should return the stack trace', () => { 47 | const err = new Error('foo'); 48 | err.stack = 'abc123'; 49 | 50 | const result = stackWithCauses(err); 51 | should.exist(result); 52 | result.should.equal('abc123'); 53 | }); 54 | 55 | it('should append causes to the stack trace', () => { 56 | const cause = new Error('foo'); 57 | cause.stack = 'abc123'; 58 | 59 | const err = new ErrorWithCause('foo', { cause }); 60 | err.stack = 'xyz789'; 61 | 62 | const result = stackWithCauses(err); 63 | should.exist(result); 64 | result.should.equal('xyz789\ncaused by: abc123'); 65 | }); 66 | 67 | it('should append VError causes to the stack trace', () => { 68 | const cause1 = new Error('Foo'); 69 | const cause2 = new ErrorWithCause('Abc', { cause: cause1 }); 70 | const cause3 = new VError(cause2, 'Bar'); 71 | const err = new ErrorWithCause('Xyz', { cause: cause3 }); 72 | 73 | const result = stackWithCauses(err); 74 | should.exist(result); 75 | 76 | result.should.match(/^ErrorWithCause: Xyz\n\s+at/); 77 | result.should.match(/\ncaused by: VError: Bar: Abc\n/); 78 | result.should.match(/\ncaused by: ErrorWithCause: Abc\n/); 79 | result.should.match(/\ncaused by: Error: Foo\n/); 80 | }); 81 | 82 | it('should not go infinite on circular error causes', () => { 83 | const cause = new ErrorWithCause('Foo'); 84 | const err = new ErrorWithCause('Bar', { cause }); 85 | 86 | cause.stack = 'abc123'; 87 | err.stack = 'xyz789'; 88 | 89 | // @ts-ignore 90 | cause.cause = err; 91 | 92 | const result = stackWithCauses(err); 93 | result.should.equal('xyz789\ncaused by: abc123\ncaused by: xyz789\ncauses have become circular...'); 94 | }); 95 | }); 96 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@voxpelli/tsconfig/node14.json", 3 | "files": [ 4 | "index.js" 5 | ], 6 | "include": [ 7 | "test/**/*.js" 8 | ], 9 | "exclude": [ 10 | "test-published-types/**/*" 11 | ] 12 | } 13 | --------------------------------------------------------------------------------