├── .bettercodehub.yml
├── .codeclimate.yml
├── .editorconfig
├── .github
├── dependabot.yml
└── workflows
│ ├── build.yml
│ ├── codeql.yml
│ └── test.yml
├── .gitignore
├── .npmignore
├── CHANGELOG.md
├── LICENSE
├── README.md
├── SECURITY.md
├── package-lock.json
├── package.json
├── src
├── custom-error.spec.ts
├── custom-error.ts
├── example
│ ├── clean-stack.ts
│ └── http-error.ts
├── factory.spec.ts
├── factory.ts
├── index.ts
├── spec.utils.ts
└── utils.ts
├── tsconfig.json
└── tslint.json
/.bettercodehub.yml:
--------------------------------------------------------------------------------
1 | component_depth: 2
2 | languages:
3 | - typescript
4 |
--------------------------------------------------------------------------------
/.codeclimate.yml:
--------------------------------------------------------------------------------
1 | version: "2"
2 | plugins:
3 | nodesecurity:
4 | enabled: true
5 | tslint:
6 | enabled: false
7 | config: tslint.json
8 | channel: beta
9 | editorconfig:
10 | enabled: false
11 | channel: beta
12 | fixme:
13 | enabled: true
14 | config:
15 | strings:
16 | - FIXME
17 | - BUG
18 | - TODO
19 |
--------------------------------------------------------------------------------
/.editorconfig:
--------------------------------------------------------------------------------
1 | root = true
2 |
3 | [*]
4 | indent_style = tab
5 | indent_size = 4
6 | end_of_line = lf
7 | charset = utf-8
8 | trim_trailing_whitespace = true
9 | insert_final_newline = true
10 |
11 | [*.json,*.yml]
12 | indent_style = space
13 | indent_size = 2
14 |
--------------------------------------------------------------------------------
/.github/dependabot.yml:
--------------------------------------------------------------------------------
1 | version: 2
2 | updates:
3 | - package-ecosystem: npm
4 | directory: /
5 | schedule:
6 | interval: weekly
--------------------------------------------------------------------------------
/.github/workflows/build.yml:
--------------------------------------------------------------------------------
1 | name: Build
2 |
3 | on:
4 | push:
5 | branches:
6 | - main
7 | pull_request:
8 | branches:
9 | - main
10 |
11 | jobs:
12 | build:
13 | runs-on: ubuntu-latest
14 | steps:
15 | - uses: actions/checkout@v3
16 | - uses: actions/setup-node@v3
17 | with:
18 | node-version: 20
19 | cache: npm
20 | cache-dependency-path: package-lock.json
21 | - run: npm ci
22 | - run: npm run build
23 | - run: npx semantic-release
24 | env:
25 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
26 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
27 |
--------------------------------------------------------------------------------
/.github/workflows/codeql.yml:
--------------------------------------------------------------------------------
1 | name: CodeQL
2 |
3 | on:
4 | push:
5 | branches: [ main, feature/* ]
6 | pull_request:
7 | branches: [ main ]
8 | schedule:
9 | - cron: '21 22 * * 4'
10 |
11 | jobs:
12 | analyze:
13 | name: Analyze
14 | runs-on: ubuntu-latest
15 | permissions:
16 | actions: read
17 | contents: read
18 | security-events: write
19 |
20 | steps:
21 | - name: Checkout repository
22 | uses: actions/checkout@v2
23 | - name: Initialize CodeQL
24 | uses: github/codeql-action/init@v2
25 | - name: Autobuild
26 | uses: github/codeql-action/autobuild@v2
27 | - name: Perform CodeQL Analysis
28 | uses: github/codeql-action/analyze@v2
29 |
--------------------------------------------------------------------------------
/.github/workflows/test.yml:
--------------------------------------------------------------------------------
1 | name: Test
2 |
3 | on:
4 | push:
5 | branches:
6 | - main
7 |
8 | jobs:
9 | coverage:
10 | runs-on: ubuntu-latest
11 | steps:
12 | - uses: actions/checkout@v3
13 | - uses: actions/setup-node@v3
14 | with:
15 | node-version: 18
16 | cache: npm
17 | cache-dependency-path: package-lock.json
18 | - run: npm ci
19 | - name: Publish code coverage
20 | uses: paambaati/codeclimate-action@v3.0.0
21 | env:
22 | CC_TEST_REPORTER_ID: ee13d547546040d53e02f832f14cbff957c249b41ce19f1b9eca55f58d1b5e02
23 | with:
24 | coverageCommand: npm run coverage
25 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .rts2_*
2 | .vscode/
3 | codeclimate-reporter
4 | coverage/
5 | dist/
6 | node_modules/
7 | npm-debug.log
8 |
--------------------------------------------------------------------------------
/.npmignore:
--------------------------------------------------------------------------------
1 | codeclimate-reporter
2 | coverage/
3 | dist/example/
4 | dist/{utils,*.spec,spec.*}.d.ts
5 | src/
6 | .*
7 | ts*.json
8 | *.tgz
9 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | ## [3.3.1](https://github.com/adriengibrat/ts-custom-error/compare/v3.3.0...v3.3.1) (2022-11-01)
2 |
3 |
4 | ### Bug Fixes
5 |
6 | * avoid error Cannot find name 'ErrorOptions' ([7535f79](https://github.com/adriengibrat/ts-custom-error/commit/7535f79f3ad8e0554a1f6062bb62e11e245ea792))
7 |
8 | # [3.3.0](https://github.com/adriengibrat/ts-custom-error/compare/v3.2.2...v3.3.0) (2022-10-22)
9 |
10 |
11 | ### Bug Fixes
12 |
13 | * add es2022 typescript lib ([e04a6b1](https://github.com/adriengibrat/ts-custom-error/commit/e04a6b1ef9870b731144670fbde5a83a5b3959c6))
14 |
15 |
16 | ### Features
17 |
18 | * **cause:** add suport for error cause ([683cf2b](https://github.com/adriengibrat/ts-custom-error/commit/683cf2bbc84f773a50dbacfe12477db13cdb6b2b))
19 |
20 | ## [3.2.2](https://github.com/adriengibrat/ts-custom-error/compare/v3.2.1...v3.2.2) (2022-08-27)
21 |
22 |
23 | ### Bug Fixes
24 |
25 | * build badge & publish semantic release branch ([b44245c](https://github.com/adriengibrat/ts-custom-error/commit/b44245ccd5fb90eb44a3d99d47080da600d67714))
26 | * publish new release ([436918e](https://github.com/adriengibrat/ts-custom-error/commit/436918e1d6d333c15da48ffd45aa22b37e213464))
27 | * renamed branch to main & updated all dev deps / CI ([7733299](https://github.com/adriengibrat/ts-custom-error/commit/773329995ac394f42199c3fcef6f5a44ad886881))
28 |
29 | ## [3.2.1](https://github.com/adriengibrat/ts-custom-error/compare/v3.2.0...v3.2.1) (2022-08-27)
30 |
31 |
32 | ### Bug Fixes
33 |
34 | * update types to support TS 4.8.2 ([80f5eb0](https://github.com/adriengibrat/ts-custom-error/commit/80f5eb08a1786ac397d6b7dd27e586e386dbe1ef))
35 |
36 | # [3.2.0](https://github.com/adriengibrat/ts-custom-error/compare/v3.1.1...v3.2.0) (2020-08-24)
37 |
38 |
39 | ### Features
40 |
41 | * **name:** Allow to redefine error name property ([94efde0](https://github.com/adriengibrat/ts-custom-error/commit/94efde0a70b62eea191bc9ff204b43101f367da8))
42 |
43 | ## [3.1.1](https://github.com/adriengibrat/ts-custom-error/compare/v3.1.0...v3.1.1) (2019-07-03)
44 |
45 |
46 | ### Bug Fixes
47 |
48 | * **package:** Remove codeclimate-reporter binary from npm package ([52a6db9](https://github.com/adriengibrat/ts-custom-error/commit/52a6db9)), closes [#32](https://github.com/adriengibrat/ts-custom-error/issues/32)
49 |
50 | # [3.1.0](https://github.com/adriengibrat/ts-custom-error/compare/v3.0.0...v3.1.0) (2019-05-17)
51 |
52 |
53 | ### Features
54 |
55 | * **log:** Behave like native Error when using console.log ([f884c51](https://github.com/adriengibrat/ts-custom-error/commit/f884c51)), closes [#30](https://github.com/adriengibrat/ts-custom-error/issues/30)
56 |
57 | # [3.0.0](https://github.com/adriengibrat/ts-custom-error/compare/v2.2.2...v3.0.0) (2019-03-15)
58 |
59 |
60 | ### chore
61 |
62 | * **licence:** Change licence to MIT instead of WTFPL ([7ff194c](https://github.com/adriengibrat/ts-custom-error/commit/7ff194c)), closes [#27](https://github.com/adriengibrat/ts-custom-error/issues/27)
63 |
64 |
65 | ### BREAKING CHANGES
66 |
67 | * **licence:** Change licence to MIT!
68 |
69 | ## [2.2.2](https://github.com/adriengibrat/ts-custom-error/compare/v2.2.1...v2.2.2) (2018-12-29)
70 |
71 |
72 | ### Bug Fixes
73 |
74 | * **release:** Fix umd minification issue, add typescript definitions for all bundles formats ([a091837](https://github.com/adriengibrat/ts-custom-error/commit/a091837))
75 |
76 |
77 | ## [2.2.1](https://github.com/adriengibrat/ts-custom-error/compare/v2.2.0...v2.2.1) (2018-04-04)
78 |
79 |
80 | ### Bug Fixes
81 |
82 | * Fix latest travis deploy fail status ([0b18352](https://github.com/adriengibrat/ts-custom-error/commit/0b18352))
83 |
84 |
85 | # [2.2.0](https://github.com/adriengibrat/ts-custom-error/compare/v2.1.0...v2.2.0) (2018-04-04)
86 |
87 |
88 | ### Features
89 |
90 | * Add custom error name support ([7791153](https://github.com/adriengibrat/ts-custom-error/commit/7791153))
91 |
92 |
93 | # [2.1.0](https://github.com/adriengibrat/ts-custom-error/compare/v2.0.0...v2.1.0) (2018-03-24)
94 |
95 |
96 | ### Features
97 |
98 | * Improve factory typings ([dc1eed6](https://github.com/adriengibrat/ts-custom-error/commit/dc1eed6))
99 |
100 |
101 |
102 |
103 | # [2.0.0](https://github.com/adriengibrat/ts-custom-error/compare/v1.0.1...v2.0.0) (2018-03-16)
104 |
105 |
106 | ### Code Refactoring
107 |
108 | * Change factory export name to customErrorFactory ([e8f51a0](https://github.com/adriengibrat/ts-custom-error/commit/e8f51a0))
109 |
110 |
111 | ### Features
112 |
113 | * Export factory Typescript Interfaces ([d03b476](https://github.com/adriengibrat/ts-custom-error/commit/d03b476))
114 |
115 |
116 | ### BREAKING CHANGES
117 |
118 | * the factory export name changed from `factory `to more expliit `customErrorFactory`
119 |
120 |
121 |
122 |
123 | ## [1.0.1](https://github.com/adriengibrat/ts-custom-error/compare/v1.0.0...v1.0.1) (2018-03-12)
124 |
125 |
126 | ### Bug Fixes
127 |
128 | * Expose constructor in prototype when using factory ([387cc8d](https://github.com/adriengibrat/ts-custom-error/commit/387cc8d))
129 |
130 |
131 |
132 |
133 | # [1.0.0](https://github.com/adriengibrat/ts-custom-error/compare/v0.0.2...v1.0.0) (2018-03-12)
134 |
135 |
136 | ### Code Refactoring
137 |
138 | * Rewrite factory to be Higher order function ([720940c](https://github.com/adriengibrat/ts-custom-error/commit/720940c))
139 |
140 | ### BREAKING CHANGES
141 |
142 | * The factory now accept a function as first parameter where previously it used an error name and a list of property keys
143 |
144 |
145 |
146 |
147 | ## 0.0.2 (2018-03-12)
148 |
149 | First release
150 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License
2 |
3 | Copyright (c) 2019 Adrien Gibrat https://github.com/adriengibrat
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in
13 | all copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21 | THE SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Typescript Custom Error
2 |
3 | [](https://www.npmjs.com/package/ts-custom-error)
4 | [](#automate-all-the-things)
5 | [](https://opensource.org/licenses/MIT)
6 | [](https://github.com/adriengibrat/ts-custom-error/actions/workflows/build.yml)
7 | [](https://github.com/adriengibrat/ts-custom-error/actions/workflows/codeql.yml)
8 | [](https://codeclimate.com/github/adriengibrat/ts-custom-error/maintainability)
9 | [](https://codeclimate.com/github/adriengibrat/ts-custom-error/test_coverage)
10 | [](http://commitizen.github.io/cz-cli/)
11 | [](https://packagephobia.now.sh/result?p=ts-custom-error)
12 | [](https://bundlephobia.com/result?p=ts-custom-error)
13 |
14 | ## Extend native Error to create custom errors
15 |
16 | `ts-custom-error` is a tiny (~500 bytes of minified & gzipped Javascript) package providing a `CustomError` class and a `customErrorFactory` function to easily extends native Error in node and evergreen browsers.
17 |
18 | It's written in Typescript and try to offer the best development and debug experiences: bundled in Javascript with Typescript definition files, map files and bundled js files for various environments: transpiled to es5 with commonjs, module and umd exports, the umd bundle is also available minified for easy import in browsers.
19 |
20 | ## Why
21 |
22 | Because [extending native Error in node and in browsers is tricky](https://stackoverflow.com/questions/1382107/whats-a-good-way-to-extend-error-in-javascript)
23 | ```js
24 | class MyError extends Error {
25 | constructor(m) {
26 | super(m)
27 | }
28 | }
29 | ```
30 | [doesn't work as expected in ES6](https://stackoverflow.com/questions/31089801/extending-error-in-javascript-with-es6-syntax-babel) and [is broken in Typescript](https://github.com/Microsoft/TypeScript-wiki/blob/master/Breaking-Changes.md#extending-built-ins-like-error-array-and-map-may-no-longer-work).
31 |
32 | ### Use `CustomError` class
33 |
34 | Simply extends and call `super` in you custom constructor.
35 |
36 | ```ts
37 | import { CustomError } from 'ts-custom-error'
38 |
39 | class HttpError extends CustomError {
40 | public constructor(
41 | public code: number,
42 | message?: string,
43 | ) {
44 | super(message)
45 | }
46 | }
47 |
48 | ...
49 |
50 | new HttpError(404, 'Not found')
51 | ```
52 | You may want more advanced contructor logic and custom methods, see [examples](https://github.com/adriengibrat/ts-custom-error/tree/main/src/example)
53 |
54 | ### Use `customErrorFactory` factory
55 |
56 | *Custom error contructor returned by the factory pass the same unit tests as Class constructor.*
57 |
58 | Factory still allows custom logic inside constructor:
59 |
60 | ```ts
61 | import { customErrorFactory } from 'ts-custom-error'
62 |
63 | const HttpError = customErrorFactory(function HttpError (code: number, message= '') {
64 | this.code = code
65 | this.message = message
66 | })
67 |
68 | ...
69 |
70 | new HttpError(404, 'Not found')
71 | ```
72 |
73 | Custom Error from `customErrorFactory` can:
74 | - Be called as a simple function
75 | ```ts
76 | HttpError(404, 'Not found')
77 | ```
78 | - Extend any native Error, using the second optional argument
79 | ```ts
80 | import { customErrorFactory } from 'ts-custom-error'
81 |
82 | const ValidationError = customErrorFactory(function ValidationError (message= 'Invalid parameter') {
83 | this.message = message
84 | }, TypeError)
85 | ```
86 |
87 | ### Known limitations
88 |
89 | #### Minification and transpilation mangle custom Error names.
90 | Unexpected results are:
91 | - Minified identifiers in place of custom Error name in Stacktrace
92 | - Wrong error recognition where using errors name (bad practice) instead of `instanceof`
93 |
94 | You may fix this behaviour by:
95 | - Using [uglifyjs options](https://github.com/mishoo/UglifyJS2/blob/harmony/README.md) `--mangle 'except=["MyError"]'` (need to specify all custom error names) or `--keep_fnames` / `--keep_classnames` (nothing to specify but your bundle size will be larger)
96 | - Setting explicitly error name:
97 |
98 | ```ts
99 | import { CustomError } from 'ts-custom-error'
100 |
101 | class MyError extends CustomError {
102 | constructor() {
103 | super()
104 | // Set name explicitly as minification can mangle class names
105 | Object.defineProperty(this, 'name', { value: 'MyError' })
106 | }
107 | }
108 | ```
109 |
110 | ```ts
111 | import { customErrorFactory } from 'ts-custom-error'
112 |
113 | const MyError = customErrorFactory(function MyError () {
114 | // Set name explicitly as minification can remove function expression names
115 | Object.defineProperty(this, 'name', { value: 'MyError' })
116 | })
117 | ```
118 |
119 | ### Usefull development commands
120 |
121 | - Watch source changes and run corresponding unit tests
122 | ```
123 | npm start
124 | ```
125 |
126 | - Run all unit tests
127 | ```
128 | npm test
129 | ```
130 |
131 | - Get coverage report
132 | ```
133 | npm run coverage
134 | ```
135 |
136 | - Format staged code and run commitizen (enforce commit message convention)
137 | ```
138 | npm run commit
139 | ```
140 |
141 | ### Automate all the things
142 |
143 | This project is maintained, it was a pet project and its first purpose was to be a playground for various external services and tools:
144 | - opinionated code style mostly inspired from [standardjs](https://standardjs.com)
145 | - automatic code formating with [prettier](https://github.com/prettier/prettier)
146 | - code quality analysis by [codeclimate](https://codeclimate.com/github/adriengibrat/ts-custom-error) & [CodeQL](https://github.com/adriengibrat/ts-custom-error/security/code-scanning)
147 | - automated continuous integration on ~~[travis](https://travis-ci.org/adriengibrat/ts-custom-error)~~ [github actions](https://github.com/adriengibrat/ts-custom-error/actions) & [Dependabot](https://github.com/adriengibrat/ts-custom-error/security/dependabot)
148 | - automated semantic versioning with [changelog](CHANGELOG.md) generation and release deployment on [npm](https://www.npmjs.com/package/ts-custom-error) and [github](https://github.com/adriengibrat/ts-custom-error/releases) thanks to [semantic-release](https://github.com/semantic-release/semantic-release)
149 |
150 | ## Licence
151 |
152 | Starting [version 3.0.0](https://github.com/adriengibrat/ts-custom-error/releases/tag/v3.0.0) this project is under [MIT licence](LICENSE), there are no code change between [version 2.2.2](https://github.com/adriengibrat/ts-custom-error/releases/tag/v2.2.2) and [version 3.0.0](https://github.com/adriengibrat/ts-custom-error/releases/tag/v3.0.0) but changing licence was considered as a breaking change. All [versions < 3.0.0](https://github.com/adriengibrat/ts-custom-error/releases) are under [WTFPL](http://www.wtfpl.net).
153 |
154 | ## Similar packages
155 |
156 | - [](https://www.npmjs.com/package/custom-error) [custom-error](https://github.com/andrezsanchez/custom-error) provides a factory with custom name and parent error
157 | - [](https://www.npmjs.com/package/custom-errors) [custom-errors](https://github.com/techjacker/custom-errors) provides a class and a factory with custom name and message, easy integration with with [express](https://github.com/expressjs/express) and (log)[https://github.com/visionmedia/log.js]
158 | - [](https://www.npmjs.com/package/custom-error-generator) [custom-error-generator](https://github.com/jproulx/node-custom-error) provides a factory with custom name, default properties and a constructor (node only)
159 | - [](https://www.npmjs.com/package/custom-error-instance) [custom-error-instance](https://github.com/Gi60s/custom-error-instance) provides a factory with custom name, properties and construction logic (! browser compatibility: redefine constructor name)
160 | - [](https://www.npmjs.com/package/node-custom-errors) [node-custom-errors](https://github.com/axyjs/node-custom-errors) provides factories to create abstract or concrete error with default message, an optional constructor function allow more custom properties/methods (node/chrome only, because no feature detection)
161 | - [](https://www.npmjs.com/package/extendable-error) [extendable-error](https://github.com/vilic/extendable-error) provides a class with clean stacktrace even in non v8 environments
162 | - [](https://www.npmjs.com/package/extendable-error-class) [extendable-error-class](https://github.com/brillout/extendable-error-class) provides simple class
163 | - [](https://www.npmjs.com/package/extend-error) [extend-error](https://github.com/jayyvis/extend-error) provides a factory attached to global Error object, allows custom name, code & message error
164 | - [](https://www.npmjs.com/package/error-extend) [error-extend](https://github.com/tilap/error-extend) provides a factory with custom name, default code & message properties, an optional init function allow more custom properties/methods
165 |
--------------------------------------------------------------------------------
/SECURITY.md:
--------------------------------------------------------------------------------
1 | # Security Policy
2 |
3 | ## Supported Versions
4 |
5 | | Version | Supported |
6 | | ------- | ------------------ |
7 | | 3.x.x | :white_check_mark: |
8 |
9 | ## Reporting a Vulnerability
10 |
11 | Please [create an issue with the "security" label](https://github.com/adriengibrat/ts-custom-error/issues/new?labels=security).
12 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "ts-custom-error",
3 | "version": "3.3.1",
4 | "description": "Extend native Error to create custom errors",
5 | "repository": "github:adriengibrat/ts-custom-error",
6 | "bugs": "https://github.com/adriengibrat/ts-custom-error/issues",
7 | "keywords": [
8 | "custom Error",
9 | "extend",
10 | "Error"
11 | ],
12 | "author": "Adrien Gibrat ",
13 | "license": "MIT",
14 | "main": "dist/custom-error.js",
15 | "module": "dist/custom-error.mjs",
16 | "unpkg": "dist/custom-error.umd.js",
17 | "types": "dist/custom-error.d.ts",
18 | "engines": {
19 | "node": ">=14.0.0"
20 | },
21 | "scripts": {
22 | "start": "jest --watch --notify",
23 | "prebuild": "rm -rf dist",
24 | "build": "tsc --build tsconfig.json && microbundle build --no-compress --entry dist/src/index.js",
25 | "postbuild": "npm run minify:umd && npm run types:concat && npm run dist:cleanup",
26 | "minify:umd": "uglifyjs --compress --output dist/custom-error.umd.js -- dist/custom-error.umd.js",
27 | "types:concat": "cat dist/src/factory.d.ts >> dist/src/custom-error.d.ts && cat dist/src/custom-error.d.ts > dist/custom-error.d.ts && cat dist/custom-error.d.ts > dist/custom-error.umd.d.ts",
28 | "dist:cleanup": "rm -rf dist/src",
29 | "test": "jest",
30 | "coverage": "jest --coverage",
31 | "commit": "lint-staged && git-cz"
32 | },
33 | "devDependencies": {
34 | "@semantic-release/changelog": "^6.0.1",
35 | "@semantic-release/git": "^10.0.1",
36 | "@types/jest": "^28.1.8",
37 | "@types/node": "^22.1.0",
38 | "commitizen": "^4.2.5",
39 | "cz-conventional-changelog": "^3.3.0",
40 | "jest": "^28.0.0",
41 | "jest-tap-reporter": "^1.9.0",
42 | "lint-staged": "^16.0.0",
43 | "microbundle": "^0.15.1",
44 | "prettier": "^3.0.0",
45 | "semantic-release": "^24.0.0",
46 | "ts-jest": "^28.0.8",
47 | "tslint": "^6.1.2",
48 | "tslint-config-prettier": "^1.18.0",
49 | "tslint-config-standard": "^9.0.0",
50 | "typescript": "^5.0.2",
51 | "uglifyjs": "^2.4.11"
52 | },
53 | "mangle": {
54 | "regex": "^(?!CustomError\b).*"
55 | },
56 | "config": {
57 | "commitizen": {
58 | "path": "cz-conventional-changelog"
59 | }
60 | },
61 | "prettier": {
62 | "useTabs": true,
63 | "semi": false,
64 | "singleQuote": true,
65 | "trailingComma": "all"
66 | },
67 | "lint-staged": {
68 | "*.ts": [
69 | "prettier --write",
70 | "tslint",
71 | "git add"
72 | ]
73 | },
74 | "jest": {
75 | "reporters": [
76 | "jest-tap-reporter"
77 | ],
78 | "testRegex": "\\.spec\\.ts$",
79 | "transform": {
80 | "^.+\\.tsx?$": "ts-jest"
81 | },
82 | "moduleFileExtensions": [
83 | "ts",
84 | "js"
85 | ],
86 | "preset": "ts-jest",
87 | "testMatch": null
88 | },
89 | "release": {
90 | "branches": [
91 | "main"
92 | ],
93 | "verifyConditions": [
94 | "@semantic-release/changelog",
95 | "@semantic-release/npm",
96 | "@semantic-release/git"
97 | ],
98 | "prepare": [
99 | "@semantic-release/changelog",
100 | "@semantic-release/npm",
101 | "@semantic-release/git"
102 | ],
103 | "publish": [
104 | "@semantic-release/npm",
105 | {
106 | "path": "@semantic-release/github",
107 | "assets": [
108 | {
109 | "path": "dist/custom-error.d.ts",
110 | "label": "Typescript typings"
111 | },
112 | {
113 | "path": "dist/custom-error.js",
114 | "label": "Common JS"
115 | },
116 | {
117 | "path": "dist/custom-error.js.map",
118 | "label": "Common JS - sourcemap"
119 | },
120 | {
121 | "path": "dist/custom-error.mjs",
122 | "label": "ES module"
123 | },
124 | {
125 | "path": "dist/custom-error.mjs.map",
126 | "label": "ES module - sourcemap"
127 | },
128 | {
129 | "path": "dist/custom-error.umd.js",
130 | "label": "UMD (minified, CDN ready)"
131 | },
132 | {
133 | "path": "dist/custom-error.umd.js.map",
134 | "label": "UMD - sourcemap"
135 | }
136 | ]
137 | }
138 | ]
139 | }
140 | }
141 |
--------------------------------------------------------------------------------
/src/custom-error.spec.ts:
--------------------------------------------------------------------------------
1 | import { checkProtoChain, checkProperties } from './spec.utils'
2 | import { CustomError } from './custom-error'
3 |
4 | test('Instance', () => checkProtoChain(CustomError, Error))
5 |
6 | test('Instance pre ES6 environment', () => {
7 | const O = Object as any
8 | const E = Error as any
9 | const setPrototypeOf = O.setPrototypeOf
10 | const captureStackTrace = E.captureStackTrace
11 | delete O.setPrototypeOf
12 | delete E.captureStackTrace
13 |
14 | checkProtoChain(CustomError, Error)
15 | checkProperties(new CustomError(), {
16 | name: 'CustomError',
17 | message: '',
18 | })
19 | O.setPrototypeOf = setPrototypeOf
20 | E.captureStackTrace = captureStackTrace
21 | })
22 |
23 | test('Extended', () => {
24 | class SubError extends CustomError { }
25 | checkProtoChain(SubError, CustomError, Error)
26 | checkProperties(new SubError('test message'), {
27 | name: 'SubError',
28 | message: 'test message',
29 | })
30 | })
31 |
32 | test('Extended with constructor', () => {
33 | class HttpError extends CustomError {
34 | constructor(public code: number, message?: string) {
35 | super(message)
36 | }
37 | }
38 | checkProtoChain(HttpError, CustomError, Error)
39 | checkProperties(new HttpError(404, 'test message'), {
40 | name: 'HttpError',
41 | code: 404,
42 | message: 'test message',
43 | })
44 | })
45 |
46 | test('Extended with name', () => {
47 | class RenamedError extends CustomError {
48 | constructor(name: string, message?: string) {
49 | super(message)
50 | Object.defineProperty(this, 'name', { value: name });
51 | }
52 | }
53 | checkProtoChain(RenamedError, CustomError, Error)
54 | checkProperties(new RenamedError('test', 'test message'), {
55 | name: 'test',
56 | message: 'test message',
57 | })
58 | })
59 |
60 | test('Basic properties', () =>
61 | checkProperties(new CustomError('my message'), {
62 | name: 'CustomError',
63 | message: 'my message',
64 | }))
65 |
66 | test('Without message', () =>
67 | checkProperties(new CustomError(), {
68 | name: 'CustomError',
69 | message: '',
70 | }))
71 |
72 | test('Native log behaviour', () =>
73 | expect(`${new CustomError('Hello')}`).toMatch('CustomError: Hello'))
74 |
75 | test('Error cause', () => {
76 | const cause = new Error()
77 | checkProperties(new CustomError('test message', { cause }), {
78 | name: 'CustomError',
79 | message: 'test message',
80 | cause,
81 | })
82 | })
83 |
--------------------------------------------------------------------------------
/src/custom-error.ts:
--------------------------------------------------------------------------------
1 | import { fixProto, fixStack } from './utils'
2 |
3 | // copy from https://github.com/microsoft/TypeScript/blob/main/lib/lib.es2022.error.d.ts
4 | // avoid typescript isue https://github.com/adriengibrat/ts-custom-error/issues/81
5 | interface ErrorOptions {
6 | cause?: unknown
7 | }
8 |
9 | /**
10 | * Allows to easily extend a base class to create custom applicative errors.
11 | *
12 | * example:
13 | * ```
14 | * class HttpError extends CustomError {
15 | * public constructor(
16 | * public code: number,
17 | * message?: string,
18 | * cause?: Error,
19 | * ) {
20 | * super(message, { cause })
21 | * }
22 | * }
23 | *
24 | * new HttpError(404, 'Not found')
25 | * ```
26 | */
27 | export class CustomError extends Error {
28 | name: string
29 |
30 | constructor(message?: string, options?: ErrorOptions) {
31 | super(message, options)
32 | // set error name as constructor name, make it not enumerable to keep native Error behavior
33 | // see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/new.target#new.target_in_constructors
34 | // see https://github.com/adriengibrat/ts-custom-error/issues/30
35 | Object.defineProperty(this, 'name', {
36 | value: new.target.name,
37 | enumerable: false,
38 | configurable: true,
39 | })
40 | // fix the extended error prototype chain
41 | // because typescript __extends implementation can't
42 | // see https://github.com/Microsoft/TypeScript-wiki/blob/master/Breaking-Changes.md#extending-built-ins-like-error-array-and-map-may-no-longer-work
43 | fixProto(this, new.target.prototype)
44 | // try to remove contructor from stack trace
45 | fixStack(this)
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/src/example/clean-stack.ts:
--------------------------------------------------------------------------------
1 | import { CustomError } from '../custom-error'
2 |
3 | const internals = () =>
4 | // HACK process binding is deprecated see https://github.com/nodejs/node/pull/22004
5 | Object.keys((process as any).binding('natives')).concat(['bootstrap_node', 'node'])
6 |
7 | const filters = (names: string[]) =>
8 | names.map(name => new RegExp(`\\(${name}\\.js:\\d+:\\d+\\)$`))
9 |
10 | const reducer = patterns => (line: string) =>
11 | patterns.some(pattern => pattern.test(line)) ? ([] as string[]) : line
12 |
13 | const cleanStack = (
14 | stack: string,
15 | filter: (line: string) => string[] | string,
16 | ) =>
17 | stack
18 | .split('\n')
19 | .reduce((stack, line) => stack.concat(filter(line)), [])
20 | .join('\n')
21 |
22 | /**
23 | * Clean Stacktrace error
24 | *
25 | * Usage:
26 | * const error = CleanError.from('My message')
27 | * console.log(error.cleanStack())
28 | */
29 | export class CleanError extends CustomError {
30 | private static node = reducer(filters(internals()))
31 |
32 | public constructor(message: string) {
33 | super(message)
34 | }
35 |
36 | public static from(message: string) {
37 | return new CleanError(message)
38 | }
39 |
40 | public cleanStack(filter = CleanError.node) {
41 | return cleanStack(this.stack, filter)
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/src/example/http-error.ts:
--------------------------------------------------------------------------------
1 | import { CustomError } from '../custom-error'
2 |
3 | /**
4 | * Http error
5 | *
6 | * Usage: throw HttpError.fromCode(404)
7 | */
8 | export class HttpError extends CustomError {
9 | protected static messages = {
10 | 400: 'Bad Request',
11 | 401: 'Unauthorized', // RFC 7235
12 | 402: 'Payment Required',
13 | 403: 'Forbidden',
14 | 404: 'Not Found',
15 | 405: 'Method Not Allowed',
16 | 406: 'Not Acceptable',
17 | 407: 'Proxy Authentication Required', // RFC 7235
18 | 408: 'Request Timeout',
19 | 409: 'Conflict',
20 | 410: 'Gone',
21 | 411: 'Length Required',
22 | 412: 'Precondition Failed', // RFC 7232
23 | 413: 'Payload Too Large', // RFC 7231
24 | 414: 'URI Too Long', // RFC 7231
25 | 415: 'Unsupported Media Type',
26 | 416: 'Range Not Satisfiable', // RFC 7233
27 | 417: 'Expectation Failed',
28 | 418: "I'm a teapot", // RFC 2324
29 | 421: 'Misdirected Request', // RFC 7540
30 | 426: 'Upgrade Required',
31 | 428: 'Precondition Required', // RFC 6585
32 | 429: 'Too Many Requests', // RFC 6585
33 | 431: 'Request Header Fields Too Large', // RFC 6585
34 | 451: 'Unavailable For Legal Reasons', // RFC 7725
35 | 500: 'Internal Server Error',
36 | 501: 'Not Implemented',
37 | 502: 'Bad Gateway',
38 | 503: 'Service Unavailable',
39 | 504: 'Gateway Timeout',
40 | 505: 'HTTP Version Not Supported',
41 | 506: 'Variant Also Negotiates', // RFC 2295
42 | 510: 'Not Extended', // RFC 2774
43 | 511: 'Network Authentication Required', // RFC 6585
44 | } as const
45 |
46 | public constructor(public code: number, message: string) {
47 | super(message)
48 | }
49 |
50 | public static fromCode(code: keyof typeof HttpError.messages) {
51 | return new HttpError(code, HttpError.messages[code])
52 | }
53 | }
54 |
--------------------------------------------------------------------------------
/src/factory.spec.ts:
--------------------------------------------------------------------------------
1 | import { customErrorFactory, GenericErrorConstructor } from './factory';
2 | import { checkProperties, checkProtoChain } from './spec.utils';
3 |
4 | const TestError = customErrorFactory(function TestError() {
5 | /**/
6 | })
7 |
8 | type Props = { code: number; message: string }
9 |
10 | const createTestErrorInstance = (parent?: GenericErrorConstructor) =>
11 | customErrorFactory(function (
12 | this: Props,
13 | code = 2,
14 | message = 'bar',
15 | ) {
16 | this.code = code
17 | this.message = message
18 | }, parent)()
19 |
20 | test('Factory instance', () => checkProtoChain(TestError, Error))
21 |
22 | test('Factory extended', () => {
23 | const SubError = customErrorFactory(function SubError() {
24 | /**/
25 | }, TestError)
26 | checkProtoChain(SubError, TestError, Error)
27 | checkProtoChain(customErrorFactory(SubError, RangeError), RangeError, Error)
28 | })
29 |
30 | test('Factory extended by class', () => {
31 | const TestError = customErrorFactory(function TestError() {
32 | /* noop */
33 | }, RangeError) as ErrorConstructor
34 | class SubError extends TestError { }
35 | checkProtoChain(SubError, TestError, RangeError, Error)
36 | })
37 |
38 | test('Factory extended with name', () => {
39 | const RenamedError = customErrorFactory(function RenamedError(this: RangeError, message: string) {
40 | this.message = message
41 | Object.defineProperty(this, 'name', { value: 'test' });
42 | }, RangeError)
43 | checkProtoChain(RenamedError, RangeError, Error)
44 | checkProperties(new RenamedError('test message'), {
45 | name: 'test',
46 | message: 'test message',
47 | })
48 | })
49 |
50 | test('Factory properties', () => {
51 | function TestError(this: Props, code = 1, message = 'foo') {
52 | this.code = code
53 | this.message = message
54 | }
55 | checkProperties(customErrorFactory(TestError)(), {
56 | name: 'TestError',
57 | code: 1,
58 | message: 'foo',
59 | })
60 |
61 | checkProperties(
62 | createTestErrorInstance(),
63 | {
64 | name: 'Error',
65 | code: 2,
66 | message: 'bar',
67 | },
68 | )
69 | checkProperties(
70 | createTestErrorInstance(RangeError),
71 | {
72 | name: 'RangeError',
73 | code: 2,
74 | message: 'bar',
75 | },
76 | )
77 | checkProperties(
78 | createTestErrorInstance(customErrorFactory(TestError)),
79 | {
80 | name: 'CustomError',
81 | code: 2,
82 | message: 'bar',
83 | },
84 | )
85 |
86 | const ArgsError = customErrorFactory(TestError)
87 | checkProperties(ArgsError(3, 'baz'), {
88 | name: 'TestError',
89 | code: 3,
90 | message: 'baz',
91 | })
92 | checkProperties(ArgsError(), {
93 | name: 'TestError',
94 | code: 1,
95 | message: 'foo',
96 | })
97 | })
98 |
99 | test('Native log behaviour', () =>
100 | expect(`${customErrorFactory(function TestError(this: Props, message) {
101 | this.message = message
102 | })('Hello')}`).toMatch('TestError: Hello'))
103 |
104 |
105 | type ErrorWithCause = { message?: string, cause?: unknown; }
106 |
107 | test('Error cause', () => {
108 | const cause = new Error()
109 | const TestError = customErrorFactory(function TestError(this: ErrorWithCause, message?: string, options?: { cause: unknown }) {
110 | this.message = message
111 | this.cause = options?.cause
112 | })
113 | checkProperties(new TestError('test message', { cause }), {
114 | name: 'TestError',
115 | message: 'test message',
116 | cause,
117 | })
118 | })
119 |
--------------------------------------------------------------------------------
/src/factory.ts:
--------------------------------------------------------------------------------
1 | import { fixStack } from './utils'
2 |
3 | export interface CustomErrorInterface extends Error {}
4 |
5 | export interface CustomErrorProperties {
6 | [property: string]: any
7 | }
8 |
9 | export interface CustomErrorConstructor<
10 | Properties extends CustomErrorProperties
11 | > extends ErrorConstructor {
12 | readonly prototype: CustomErrorInterface
13 | new (...args: any[]): CustomErrorInterface & Properties
14 | (...args: any[]): CustomErrorInterface & Properties
15 | }
16 |
17 | export type GenericErrorConstructor =
18 | | ErrorConstructor
19 | | EvalErrorConstructor
20 | | RangeErrorConstructor
21 | | ReferenceErrorConstructor
22 | | SyntaxErrorConstructor
23 | | TypeErrorConstructor
24 | | URIErrorConstructor
25 | | CustomErrorConstructor
26 |
27 | type CustomErrorFunction = (this: Properties, ...args: any[]) => void
28 |
29 | /**
30 | * Allows to easily extend native errors to create custom applicative errors.
31 | *
32 | * example:
33 | * ```
34 | * const HttpError = customErrorFactory(function (code: number, message= '') {
35 | * this.code = code
36 | * this.message = message
37 | * })
38 | *
39 | * new HttpError(404, 'Not found')
40 | * ```
41 | */
42 | export function customErrorFactory(
43 | fn: CustomErrorFunction,
44 | parent: GenericErrorConstructor = Error,
45 | ) {
46 | function CustomError(this: CustomErrorInterface & Properties, ...args: any[]): void {
47 | // allow simple function call
48 | if (!(this instanceof CustomError)) return new CustomError(...args)
49 | // apply super
50 | parent.apply(this, args)
51 | // set name from custom fn, default to parent Error name
52 | Object.defineProperty(this, 'name', {
53 | value: fn.name || parent.name,
54 | enumerable: false,
55 | configurable: true,
56 | })
57 | // apply custom fn
58 | fn.apply(this, args)
59 | // try to remove contructor from stack trace
60 | fixStack(this, CustomError)
61 | }
62 |
63 | return Object.defineProperties(CustomError, {
64 | prototype: {
65 | value: Object.create(parent.prototype, {
66 | constructor: {
67 | value: CustomError,
68 | writable: true,
69 | configurable: true,
70 | },
71 | }),
72 | },
73 | }) as CustomErrorConstructor
74 | }
75 |
--------------------------------------------------------------------------------
/src/index.ts:
--------------------------------------------------------------------------------
1 | export * from './custom-error'
2 | export * from './factory'
3 |
--------------------------------------------------------------------------------
/src/spec.utils.ts:
--------------------------------------------------------------------------------
1 | type AnyConstructor = {
2 | new (...args): any
3 | }
4 |
5 | export const checkProtoChain = (
6 | contructor: AnyConstructor,
7 | ...chain: AnyConstructor[]
8 | ) => {
9 | const error = new contructor()
10 | expect(error).toBeInstanceOf(contructor)
11 | chain.forEach(type => expect(error).toBeInstanceOf(type))
12 | }
13 |
14 | type CheckedProperties = {
15 | [key: string]: any
16 | name: string
17 | message: string
18 | }
19 | export const checkProperties = (error: any, properties: CheckedProperties) => {
20 | Object.keys(properties).forEach(property =>
21 | expect(error[property]).toBe(properties[property]),
22 | )
23 | const stackPattern = properties.message
24 | ? `${properties.name}: ${properties.message}`
25 | : new RegExp(`^${properties.name}\\b`)
26 | expect(error.stack).toMatch(stackPattern)
27 | }
28 |
--------------------------------------------------------------------------------
/src/utils.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Fix the prototype chain of the error
3 | *
4 | * Use Object.setPrototypeOf
5 | * Support ES6 environments
6 | *
7 | * Fallback setting __proto__
8 | * Support IE11+, see https://docs.microsoft.com/en-us/scripting/javascript/reference/javascript-version-information
9 | */
10 | export function fixProto(target: Error, prototype: {}) {
11 | const setPrototypeOf: Function = (Object as any).setPrototypeOf
12 | setPrototypeOf
13 | ? setPrototypeOf(target, prototype)
14 | : ((target as any).__proto__ = prototype)
15 | }
16 |
17 | /**
18 | * Capture and fix the error stack when available
19 | *
20 | * Use Error.captureStackTrace
21 | * Support v8 environments
22 | */
23 | export function fixStack(target: Error, fn: Function = target.constructor) {
24 | const captureStackTrace: Function = (Error as any).captureStackTrace
25 | captureStackTrace && captureStackTrace(target, fn)
26 | }
27 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "lib": [
4 | "ES6",
5 | "ES2022",
6 | "ES2022.error"
7 | ],
8 | "target": "ES5",
9 | "module": "ESNext",
10 | "moduleResolution": "Node",
11 | "esModuleInterop": true,
12 | "sourceMap": true,
13 | "declaration": true,
14 | "outDir": "dist/src",
15 | "removeComments": true
16 | },
17 | "exclude": [
18 | "node_modules/**",
19 | "src/example/*",
20 | "src/*.spec.*",
21 | "src/spec.*"
22 | ],
23 | "include": [
24 | "src/**/*"
25 | ]
26 | }
27 |
--------------------------------------------------------------------------------
/tslint.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": [
3 | "tslint-config-standard",
4 | "tslint-config-prettier"
5 | ]
6 | }
7 |
--------------------------------------------------------------------------------