├── .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 |

8 |
9 |
10 |
11 |
12 | Helpers and [ponyfill](https://ponyfill.com/) for [Error Causes](https://github.com/tc39/proposal-error-cause)
13 |
14 | [](https://www.npmjs.com/package/pony-cause)
15 | [](https://www.npmjs.com/package/pony-cause)
16 | [](https://github.com/voxpelli/badges-cjs-esm)
17 | [](https://github.com/voxpelli/types-in-js)
18 | [](https://github.com/neostandard/neostandard)
19 | [](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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------