├── .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 | [![NPM version](https://img.shields.io/npm/v/ts-custom-error.svg?colorB=green)](https://www.npmjs.com/package/ts-custom-error) 4 | [![stable maintenance](https://img.shields.io/badge/maintain-stable-green.svg?logo=github)](#automate-all-the-things) 5 | [![License: MIT](https://img.shields.io/badge/License-MIT-green.svg)](https://opensource.org/licenses/MIT) 6 | [![Build Status](https://github.com/adriengibrat/ts-custom-error/actions/workflows/build.yml/badge.svg)](https://github.com/adriengibrat/ts-custom-error/actions/workflows/build.yml) 7 | [![CodeQL](https://github.com/github/docs/actions/workflows/codeql.yml/badge.svg)](https://github.com/adriengibrat/ts-custom-error/actions/workflows/codeql.yml) 8 | [![Maintainability](https://api.codeclimate.com/v1/badges/eb4eb956bc028c49f7aa/maintainability)](https://codeclimate.com/github/adriengibrat/ts-custom-error/maintainability) 9 | [![Test Coverage](https://api.codeclimate.com/v1/badges/eb4eb956bc028c49f7aa/test_coverage)](https://codeclimate.com/github/adriengibrat/ts-custom-error/test_coverage) 10 | [![Commitizen friendly](https://img.shields.io/badge/commitizen-friendly-brightgreen.svg)](http://commitizen.github.io/cz-cli/) 11 | [![Install size](https://badgen.net/packagephobia/install/ts-custom-error)](https://packagephobia.now.sh/result?p=ts-custom-error) 12 | [![Bundle size](https://badgen.net/bundlephobia/minzip/ts-custom-error?color=green)](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 | - [![custom-error](https://badge.fury.io/js/custom-error.svg)](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 | - [![custom-errors](https://badge.fury.io/js/custom-errors.svg)](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 | - [![custom-error-generator](https://badge.fury.io/js/custom-error-generator.svg)](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 | - [![custom-error-instance](https://badge.fury.io/js/custom-error-instance.svg)](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 | - [![node-custom-errors](https://badge.fury.io/js/node-custom-errors.svg)](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 | - [![extendable-error](https://badge.fury.io/js/extendable-error.svg)](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 | - [![extendable-error-class](https://badge.fury.io/js/extendable-error-class.svg)](https://www.npmjs.com/package/extendable-error-class) [extendable-error-class](https://github.com/brillout/extendable-error-class) provides simple class 163 | - [![extend-error](https://badge.fury.io/js/extend-error.svg)](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 | - [![error-extend](https://badge.fury.io/js/eerror-extend.svg)](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 | --------------------------------------------------------------------------------