├── .commitlintrc.json ├── .git-hooks ├── commit-msg └── pre-commit ├── .github └── workflows │ ├── README.md │ ├── ci.yml │ └── release.yml ├── .gitignore ├── .lintstagedrc.json ├── CHANGELOG.md ├── LICENSE ├── README.md ├── biome.json ├── mise.toml ├── package.json ├── pnpm-lock.yaml ├── pnpm-workspace.yaml ├── setup-git-hooks.js ├── src ├── examples.ts ├── main.test.ts └── main.ts ├── tsconfig.json └── tsup.config.ts /.commitlintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "@commitlint/config-conventional" 4 | ], 5 | "rules": { 6 | "body-max-line-length": [ 7 | 1, 8 | "always", 9 | 500 10 | ] 11 | } 12 | } -------------------------------------------------------------------------------- /.git-hooks/commit-msg: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -e 3 | 4 | echo "⚠️ Running Commitlint to validate commit message..." 5 | 6 | # Source mise if available 7 | if [ -f "$HOME/.mise/bin/mise" ]; then 8 | eval "$($HOME/.mise/bin/mise activate bash)" 9 | elif [ -f "$HOME/.config/mise/bin/mise" ]; then 10 | eval "$($HOME/.config/mise/bin/mise activate bash)" 11 | elif command -v mise &>/dev/null; then 12 | eval "$(mise activate bash)" 13 | fi 14 | 15 | # Run commitlint 16 | if command -v npx &>/dev/null; then 17 | npx --no-install commitlint --edit $1 || { 18 | echo "❌ Commit message validation failed." 19 | exit 1 20 | } 21 | else 22 | echo "❌ npx not found in PATH. Make sure Node.js is properly set up." 23 | echo "Skipping commit message validation." 24 | fi 25 | 26 | echo "✅ Commit message validation passed!" -------------------------------------------------------------------------------- /.git-hooks/pre-commit: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -e 3 | 4 | echo "⚠️ Running Linting and Formatting prior to committing..." 5 | 6 | # Make sure we're using the locally installed version 7 | export PATH="./node_modules/.bin:$PATH" 8 | 9 | # Check if there are any staged JS/TS files 10 | STAGED_FILES=$(git diff --cached --name-only --diff-filter=ACM | grep -E '\*.(js|ts)$' || true) 11 | 12 | if [ -n "$STAGED_FILES" ]; then 13 | # Only run lint-staged if there are matching files 14 | if command -v lint-staged &>/dev/null; then 15 | lint-staged 16 | else 17 | echo "❌ lint-staged not found. Please run 'pnpm install' to install dependencies." 18 | exit 1 19 | fi 20 | else 21 | echo "→ No JS/TS files to check." 22 | fi 23 | 24 | echo "✅ Linting and formatting passed!" 25 | -------------------------------------------------------------------------------- /.github/workflows/README.md: -------------------------------------------------------------------------------- 1 | # GitHub Workflows 2 | 3 | This directory contains GitHub Actions workflows for automating CI/CD processes for the @fuzzy-street/errors package. 4 | 5 | ## Workflows 6 | 7 | ### CI (`ci.yml`) 8 | 9 | This workflow runs on: 10 | - Every push to the `main` branch 11 | - Every pull request to the `main` branch 12 | 13 | It performs the following steps: 14 | 1. Checkout the code 15 | 2. Set up Node.js and pnpm 16 | 3. Install dependencies 17 | 4. Run linting with Biome 18 | 5. Run type checking with TypeScript 19 | 6. Build the package 20 | 7. Run tests 21 | 22 | Use this workflow to ensure code quality and prevent breaking changes. 23 | 24 | ### Release (`release.yml`) 25 | 26 | This workflow runs when a tag with the pattern `v*` is pushed to the repository (e.g., `v1.0.0`). 27 | 28 | It performs the following steps: 29 | 1. Checkout the code 30 | 2. Set up Node.js and pnpm 31 | 3. Install dependencies 32 | 4. Run linting, type checking, build, and tests 33 | 5. Generate a changelog based on conventional commits 34 | 6. Create a GitHub release with the changelog 35 | 7. Publish the package to npm 36 | 37 | ## Release Process 38 | 39 | To release a new version: 40 | 41 | 1. Make sure all changes are committed to the `main` branch 42 | 2. Run one of the release commands: 43 | ```bash 44 | # For patch releases (bug fixes) 45 | pnpm release:patch 46 | 47 | # For minor releases (new features) 48 | pnpm release:minor 49 | 50 | # For major releases (breaking changes) 51 | pnpm release:major 52 | ``` 53 | 3. Push the tags to GitHub: 54 | ```bash 55 | git push --follow-tags origin main 56 | ``` 57 | 4. The release workflow will automatically: 58 | - Create a GitHub release with the changelog 59 | - Publish the package to npm 60 | 61 | ## Configuration 62 | 63 | ### NPM Token 64 | 65 | To publish to npm, the workflow uses the `NPM_TOKEN` secret. 66 | 67 | This token should be added to your GitHub repository secrets settings: 68 | 1. Go to your repository on GitHub 69 | 2. Navigate to Settings > Secrets and variables > Actions 70 | 3. Click "New repository secret" 71 | 4. Name: `NPM_TOKEN` 72 | 5. Value: Your npm access token with publish permissions 73 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: [ main ] 6 | pull_request: 7 | branches: [ main ] 8 | 9 | jobs: 10 | build: 11 | runs-on: ubuntu-latest 12 | 13 | steps: 14 | - uses: actions/checkout@v4 15 | 16 | - name: Use Node.js 17 | uses: actions/setup-node@v4 18 | with: 19 | node-version: '22.x' 20 | 21 | - name: Setup PNPM 22 | uses: pnpm/action-setup@v2 23 | with: 24 | version: 10.7.1 25 | 26 | - name: Install dependencies 27 | run: pnpm install --frozen-lockfile 28 | 29 | - name: Format 30 | run: pnpm format 31 | 32 | - name: Lint 33 | run: pnpm lint 34 | 35 | - name: Test 36 | run: pnpm test:ci 37 | 38 | - name: Build 39 | run: pnpm build 40 | 41 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | push: 5 | tags: 6 | - 'v*' 7 | 8 | permissions: 9 | contents: write 10 | 11 | jobs: 12 | release: 13 | runs-on: ubuntu-latest 14 | 15 | steps: 16 | - uses: actions/checkout@v4 17 | with: 18 | fetch-depth: 0 19 | 20 | - name: Use Node.js 21 | uses: actions/setup-node@v4 22 | with: 23 | node-version: '22.x' 24 | registry-url: 'https://registry.npmjs.org' 25 | 26 | - name: Setup PNPM 27 | uses: pnpm/action-setup@v2 28 | with: 29 | version: 10.7.1 30 | 31 | - name: Install dependencies 32 | run: pnpm install --frozen-lockfile 33 | 34 | # - name: Lint 35 | # run: pnpm exec biome check . 36 | 37 | # - name: Type check 38 | # run: pnpm check 39 | - name: Test 40 | run: pnpm test 41 | 42 | - name: Build 43 | run: pnpm build 44 | 45 | 46 | - name: Generate changelog 47 | id: changelog 48 | uses: TriPSs/conventional-changelog-action@v5 49 | with: 50 | github-token: ${{ secrets.GITHUB_TOKEN }} 51 | git-push: 'false' # We're already pushing with a tag 52 | skip-version-file: 'true' # We're using a tag, not generating a new version 53 | skip-commit: 'true' # Skip creating a release commit 54 | skip-git-pull: 'true' # No need to pull as we just checked out 55 | create-summary: 'true' # Creates a summary in GitHub Actions 56 | output-file: 'CHANGELOG.md' 57 | 58 | - name: Create GitHub Release 59 | uses: softprops/action-gh-release@v2.2.1 60 | if: ${{ steps.changelog.outputs.skipped == 'false' }} 61 | with: 62 | body: ${{ steps.changelog.outputs.clean_changelog }} 63 | 64 | - name: Publish to npm 65 | run: pnpm publish --no-git-checks 66 | env: 67 | NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | .pnpm-debug.log* 9 | 10 | # Diagnostic reports (https://nodejs.org/api/report.html) 11 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 12 | 13 | # Runtime data 14 | pids 15 | *.pid 16 | *.seed 17 | *.pid.lock 18 | 19 | # Directory for instrumented libs generated by jscoverage/JSCover 20 | lib-cov 21 | 22 | # Coverage directory used by tools like istanbul 23 | coverage 24 | *.lcov 25 | 26 | # nyc test coverage 27 | .nyc_output 28 | 29 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 30 | .grunt 31 | 32 | # Bower dependency directory (https://bower.io/) 33 | bower_components 34 | 35 | # node-waf configuration 36 | .lock-wscript 37 | 38 | # Compiled binary addons (https://nodejs.org/api/addons.html) 39 | build/Release 40 | 41 | # Dependency directories 42 | node_modules/ 43 | jspm_packages/ 44 | 45 | # Snowpack dependency directory (https://snowpack.dev/) 46 | web_modules/ 47 | 48 | # TypeScript cache 49 | *.tsbuildinfo 50 | 51 | # Optional npm cache directory 52 | .npm 53 | 54 | # Optional eslint cache 55 | .eslintcache 56 | 57 | # Optional stylelint cache 58 | .stylelintcache 59 | 60 | # Microbundle cache 61 | .rpt2_cache/ 62 | .rts2_cache_cjs/ 63 | .rts2_cache_es/ 64 | .rts2_cache_umd/ 65 | 66 | # Optional REPL history 67 | .node_repl_history 68 | 69 | # Output of 'npm pack' 70 | *.tgz 71 | 72 | # Yarn Integrity file 73 | .yarn-integrity 74 | 75 | # dotenv environment variable files 76 | .env 77 | .env.development.local 78 | .env.test.local 79 | .env.production.local 80 | .env.local 81 | 82 | # parcel-bundler cache (https://parceljs.org/) 83 | .cache 84 | .parcel-cache 85 | 86 | # Next.js build output 87 | .next 88 | out 89 | 90 | # Nuxt.js build / generate output 91 | .nuxt 92 | dist 93 | 94 | # Gatsby files 95 | .cache/ 96 | # Comment in the public line in if your project uses Gatsby and not Next.js 97 | # https://nextjs.org/blog/next-9-1#public-directory-support 98 | # public 99 | 100 | # vuepress build output 101 | .vuepress/dist 102 | 103 | # vuepress v2.x temp and cache directory 104 | .temp 105 | .cache 106 | 107 | # vitepress build output 108 | **/.vitepress/dist 109 | 110 | # vitepress cache directory 111 | **/.vitepress/cache 112 | 113 | # Docusaurus cache and generated files 114 | .docusaurus 115 | 116 | # Serverless directories 117 | .serverless/ 118 | 119 | # FuseBox cache 120 | .fusebox/ 121 | 122 | # DynamoDB Local files 123 | .dynamodb/ 124 | 125 | # TernJS port file 126 | .tern-port 127 | 128 | # Stores VSCode versions used for testing VSCode extensions 129 | .vscode-test 130 | 131 | # yarn v2 132 | .yarn/cache 133 | .yarn/unplugged 134 | .yarn/build-state.yml 135 | .yarn/install-state.gz 136 | .pnp.* 137 | -------------------------------------------------------------------------------- /.lintstagedrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "*.{js,ts}": [ 3 | "biome check --write" 4 | ] 5 | } -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines. 4 | 5 | ## [1.1.0](https://github.com/fuzzy-st/errors/compare/v1.0.0...v1.1.0) (2025-04-25) 6 | 7 | ## 1.0.0 (2025-04-06) 8 | 9 | 10 | ### Features 11 | 12 | * add `checkInstance(error,instance)` type guard and helper method along with `toJSON` for better object serialization ([d1c0a53](https://github.com/fuzzy-st/errors/commit/d1c0a53b8cce0adcb009c78f516d58e7b0a9f523)) 13 | * add CI workflow for automated build, linting, and testing ([3a755e2](https://github.com/fuzzy-st/errors/commit/3a755e2a20c12374e615868cf09e747b18a85465)) 14 | * add commit-msg hook for validating commit messages with commitlint ([97e681e](https://github.com/fuzzy-st/errors/commit/97e681e3baa94ac627e6b9938f2eaed9e8c14ecf)) 15 | * add commitlint configuration for standardized commit messages ([9e06df0](https://github.com/fuzzy-st/errors/commit/9e06df02f0d8c566b2a8affece53597d518d9924)) 16 | * add comprehensive examples for custom error handling utility ([2a5be51](https://github.com/fuzzy-st/errors/commit/2a5be51f10bd11d676bcfc9a07760096a63fcd65)) 17 | * add cut-release script for automated git tagging and pushing ([659cb03](https://github.com/fuzzy-st/errors/commit/659cb03ef96cec62e45c3ace818f203399682f3c)) 18 | * add examples script to package.json for easier demonstration ([13d2c40](https://github.com/fuzzy-st/errors/commit/13d2c403b96ec027171416e0bc2f6a6b840d946d)) 19 | * add lint-staged configuration for biome checks on staged files ([82e469b](https://github.com/fuzzy-st/errors/commit/82e469bb733a411e71c07df2aec5fae911b86190)) 20 | * add pre-commit deps and scripts ([39e9733](https://github.com/fuzzy-st/errors/commit/39e97333cf1c9f9f3a5800345ef82415109a6017)) 21 | * add pre-commit hook for linting and formatting staged files ([41d6ab8](https://github.com/fuzzy-st/errors/commit/41d6ab827a62f07b7ff2de86f0b56ac954847a1d)) 22 | * add README for GitHub workflows detailing CI and release processes ([320a1f3](https://github.com/fuzzy-st/errors/commit/320a1f3653bcd1d1b301c3aa0fdc00be83b8063f)) 23 | * add release workflow for automated versioning and publishing to npm ([b5f35d7](https://github.com/fuzzy-st/errors/commit/b5f35d7a8ae63101ccf93cd080e30e8b7091a69d)) 24 | * add setup script for configuring git hooks ([e47f429](https://github.com/fuzzy-st/errors/commit/e47f429437ad47803c6957f837e31cf8b6c3ce56)) 25 | * **biome:** add complexity rule for `noBannedType`s in biome.json ([cef6b27](https://github.com/fuzzy-st/errors/commit/cef6b2782ebef91ce5e650b9ee9982849f0db91d)) 26 | * **build:** update entry point to `main.ts` and enable minification, code splitting, and tree shaking ([6bcc0a3](https://github.com/fuzzy-st/errors/commit/6bcc0a340eae381acaf883ebfbc5fcf9f0327676)) 27 | * Complete Enhanced Error Hierarchy implementation with expanded API ([f6fd10c](https://github.com/fuzzy-st/errors/commit/f6fd10cdbbcae8fd205d6372be78ad147e6f87ec)) 28 | * **config:** Add `biome.json` configuration for code formatting and linting ([51f4572](https://github.com/fuzzy-st/errors/commit/51f4572ba5365eda3c8b3518bc861eb8647aba77)) 29 | * **config:** Add `tsup.config.ts` for bundling configuration ([1d41f06](https://github.com/fuzzy-st/errors/commit/1d41f06e8c707d8577c31849ec1000e3e814ab9d)) 30 | * **config:** Add TypeScript configuration file `tsconfig.json` ([d349397](https://github.com/fuzzy-st/errors/commit/d3493977db539c20c4153754248dcbae08f6b783)) 31 | * **docs:** Update README with comprehensive overview ([a31adb7](https://github.com/fuzzy-st/errors/commit/a31adb78af46240ead5df058225bb106fd8271d8)) 32 | * enhance pre-commit hook to check for staged JS/TS files before linting ([ceccb3c](https://github.com/fuzzy-st/errors/commit/ceccb3c7f3fcabe62b819312ce0b4b62b4baac9c)) 33 | * **init:** Add `mise.toml` configuration for environment tools ([e8a069b](https://github.com/fuzzy-st/errors/commit/e8a069ba5ea411304c282d45886500817bc27fed)) 34 | * **init:** Add initial `pnpm-workspace.yaml` and `package.json` for error handling library ([bf5dcbc](https://github.com/fuzzy-st/errors/commit/bf5dcbce8e464226e5a9c5a8278d45d79c6bfecc)) 35 | * **test:** add comprehensive unit tests for `createCustomError` and `checkInstance`. ([aa92c9a](https://github.com/fuzzy-st/errors/commit/aa92c9afedb1cda598be2770c62510bc38c002b6)) 36 | * **test:** update tests to reflect changes made to API surface ([ca63ba5](https://github.com/fuzzy-st/errors/commit/ca63ba5f113f09f0fef51c6f9c256e249be4a13d)) 37 | * update test suite with more requirements ([af585a6](https://github.com/fuzzy-st/errors/commit/af585a6a7dc6bcdca42fc6fa46d4023abb99b3d9)) 38 | * WOCKA WOCKA! Add enhanced custom error handling utility ([47396a3](https://github.com/fuzzy-st/errors/commit/47396a3955a2233e319dd28c67cbcd276604e57f)) 39 | 40 | 41 | ### Bug Fixes 42 | 43 | * update biome check command from --apply to --write in lint-staged configuration ([44c54fd](https://github.com/fuzzy-st/errors/commit/44c54fdab7ab53bcf94fbf8b09188db204990c11)) 44 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 Fuzzy St. 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 all 13 | 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 THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Custom Errors 2 | 3 | A *very* powerful, fully **type-safe**, *dependency free* utility for creating rich **custom errors**. 4 | Complete with: 5 | - Hierarchical error classes 6 | - Advanced context tracking 7 | - Inheritance and diagnostic capabilities 8 | - Performance optimizations 9 | - Circular reference protection 10 | 11 | Its a *fuzzy* sort of idea, that by having a form of *contextual-based error support* we can **craft better consequences** when an `error` is eventually *thrown* in our near perfect code-bases. 12 | 13 | ## 🔍 Overview 14 | 15 | This library aims to provide an elegant solution for creating simple to sophisticated `errors` in TypeScript applications. It looked to solve some of the common problem of with the passage of contextual information to our `errors` while maintaining the important type safety along with proper inheritance and their relationships. 16 | 17 | Unlike standard JavaScript `class Error`'s or basic custom error extensions (for which there are many, and all great sources of inspiration), this wee library seeks to enable us with the following: 18 | 19 | - **Error hierarchies** that maintain proper inheritance relationships 20 | - **Rich contextual data** with strong TypeScript typing 21 | - **Parent-child error relationships** for comprehensive error chains 22 | - **Context inheritance** from parent errors to child errors 23 | - **Advanced error analysis tools** for debugging and logging 24 | - **Performance optimizations** for high-frequency error creation 25 | 26 | ## ✨ Features 27 | 28 | - 🧙‍♂️ **Type-Safe Contextual Data** - Associate strongly-typed contextual `causes` with errors 29 | - 🔄 **Hierarchical Error Classes** - Build complex error taxonomies with proper inheritance 30 | - 👪 **Parent-Child Relationships** - Create and traverse parent-child error chains 31 | - 🧬 **Inheritance Tracking** - Maintain complete inheritance hierarchies 32 | - 🔍 **Error Inspection** - Utilities for exploring error contexts and hierarchies 33 | - 📝 **Customizable Serialization** - Enhanced `.toString()` and `.toJSON()` for better logging 34 | - 🔁 **Circular Reference Protection** - Safe traversal of complex error hierarchies 35 | - ⚡ **Performance Optimizations** - Fast error creation for high-frequency scenarios (~40% faster) 36 | - 💥 **Collision Detection** - Configurable strategies for handling property name collisions 37 | - 🏦 **Registry Management** - Access to all registered error classes for global management 38 | - 💻 **Developer-Friendly API** - A very simple yet powerful interface that us developers deserve 39 | - 🆓 **Dependency Free** - Yes, its completely devoid of any external dependencies 40 | - 🪖 **Battle tested** - Rigiourly tested API surface, trusted to last. 41 | - 💚 **Runtime & Environment** friendly, it can be run literally anywhere; In the browser, on the server, perhaps in your little IOT, heck even in your cup of tea! 42 | 43 | 44 | ## 📦 Installation 45 | 46 | ```bash 47 | npm install @fuzzy-street/errors 48 | # or 49 | yarn add @fuzzy-street/errors 50 | # or 51 | pnpm add @fuzzy-street/errors 52 | ``` 53 | 54 | ## 🚀 Quick Start 55 | 56 | ```typescript 57 | import { createCustomError, isError } from '@fuzzy-street/errors'; 58 | 59 | // Create a basic error class 60 | const ApiError = createCustomError<{ 61 | statusCode: number; 62 | endpoint: string; 63 | }>("ApiError", ["statusCode", "endpoint"]); 64 | 65 | // Create a derived error class 66 | const NetworkError = createCustomError<{ 67 | retryCount: number; 68 | }, typeof ApiError>( 69 | "NetworkError", 70 | ["retryCount"], 71 | ApiError 72 | ); 73 | 74 | // Throw with complete context 75 | try { 76 | throw new NetworkError({ 77 | message: "Failed to connect to API", 78 | cause: { 79 | statusCode: 503, 80 | endpoint: "/api/users", 81 | retryCount: 3 82 | } 83 | }); 84 | } catch (error) { 85 | if (isError(error, NetworkError)) { 86 | // Direct property access with full type safety 87 | console.log(`Status code: ${error.statusCode}`); 88 | console.log(`Retries attempted: ${error.retryCount}`); 89 | 90 | // View the error hierarchy 91 | console.log(error.toString()); 92 | } 93 | } 94 | ``` 95 | 96 | ## 📚 Usage Guide 97 | 98 | ### Creating Basic Error Classes 99 | 100 | ```typescript 101 | // Define an error with typed context 102 | const ConfigError = createCustomError<{ 103 | configFile: string; 104 | missingKey: string; 105 | }>("ConfigError", ["configFile", "missingKey"]); 106 | 107 | // Create an instance 108 | const error = new ConfigError({ 109 | message: "Missing required configuration key", 110 | cause: { 111 | configFile: "/etc/app/config.json", 112 | missingKey: "API_SECRET" 113 | }, 114 | captureStack: true // Capture stack trace 115 | }); 116 | ``` 117 | 118 | ### Building Error Hierarchies 119 | 120 | ```typescript 121 | // Base application error 122 | const AppError = createCustomError<{ 123 | appName: string; 124 | version: string; 125 | }>("AppError", ["appName", "version"]); 126 | 127 | // File system error extends AppError 128 | const FileSystemError = createCustomError<{ 129 | path: string; 130 | operation: "read" | "write" | "delete"; 131 | }, typeof AppError>( 132 | "FileSystemError", 133 | ["path", "operation"], 134 | AppError 135 | ); 136 | 137 | // Permission error extends FileSystemError 138 | const PermissionError = createCustomError<{ 139 | requiredPermission: string; 140 | currentUser: string; 141 | }, typeof FileSystemError>( 142 | "PermissionError", 143 | ["requiredPermission", "currentUser"], 144 | FileSystemError 145 | ); 146 | 147 | // Usage: complete context inheritance 148 | throw new PermissionError({ 149 | message: "Cannot write to file: permission denied", 150 | cause: { 151 | // PermissionError context 152 | requiredPermission: "WRITE", 153 | currentUser: "guest", 154 | 155 | // FileSystemError context 156 | path: "/var/data/users.json", 157 | operation: "write", 158 | 159 | // AppError context 160 | appName: "MyApp", 161 | version: "1.2.3" 162 | } 163 | }); 164 | ``` 165 | 166 | ### Error Handling with Type-Safe Context Access 167 | 168 | ```typescript 169 | try { 170 | // Code that might throw PermissionError 171 | } catch (error) { 172 | // Type-safe instance checking with proper TypeScript inference 173 | if (isError(error, PermissionError)) { 174 | // Direct access to all properties with full type safety 175 | console.log(`User '${error.currentUser}' lacks '${error.requiredPermission}' permission`); 176 | console.log(`Operation '${error.operation}' failed on '${error.path}'`); 177 | console.log(`App: ${error.appName} v${error.version}`); 178 | 179 | // Alternatively, use getContext 180 | const fullContext = PermissionError.getContext(error); 181 | console.log(`Complete context:`, fullContext); 182 | 183 | // Get only PermissionError context (not parent context) 184 | const permContext = PermissionError.getContext(error, { 185 | includeParentContext: false 186 | }); 187 | } 188 | } 189 | ``` 190 | 191 | ### Analyzing Error Hierarchies 192 | 193 | ```typescript 194 | try { 195 | // Code that might throw errors 196 | } catch (error) { 197 | if (error instanceof AppError) { 198 | // Get the full error hierarchy with context 199 | const hierarchy = AppError.getErrorHierarchy(error); 200 | console.log(JSON.stringify(hierarchy, null, 2)); 201 | 202 | // Follow the parent chain (with circular reference protection) 203 | const parentChain = AppError.followParentChain(error); 204 | console.log(`Error chain depth: ${parentChain.length}`); 205 | 206 | // Log the complete error with context 207 | console.log(error.toString()); 208 | } 209 | } 210 | ``` 211 | 212 | ### Handling Errors with Parent References 213 | 214 | ```typescript 215 | try { 216 | try { 217 | throw new DatabaseError({ 218 | message: "Database connection failed", 219 | cause: { 220 | dbName: "users", 221 | connectionString: "postgres://localhost:5432/users" 222 | } 223 | }); 224 | } catch (dbError) { 225 | // Create a new error with the database error as the parent 226 | throw new ApiError({ 227 | message: "Failed to fetch user data", 228 | parent: dbError, // Pass error as parent to establish parent relationship 229 | captureStack: true 230 | }); 231 | } 232 | } catch (error) { 233 | if (isError(error, ApiError)) { 234 | console.log(error.toString()); 235 | 236 | // Access parent error 237 | if (error.parent && isError(error.parent, DatabaseError)) { 238 | // Direct property access 239 | console.log(`Failed to connect to: ${error.parent.dbName}`); 240 | 241 | // Or use context getter 242 | const dbContext = DatabaseError.getContext(error.parent); 243 | console.log(`Connection string: ${dbContext.connectionString}`); 244 | } 245 | } 246 | } 247 | ``` 248 | 249 | ### High-Performance Error Creation 250 | 251 | ```typescript 252 | function logApiError(statusCode, endpoint) { 253 | // 🚫 For performance-critical paths, use createFast (40% faster) 254 | // Fast error creation without stack traces or extra processing 255 | const error = ApiError.createFast("API request failed", { 256 | statusCode, 257 | endpoint 258 | }); 259 | 260 | errorLogger.log(error); 261 | } 262 | ``` 263 | 264 | ### Accessing Error Registry 265 | 266 | ```typescript 267 | import { getErrorClass, listErrorClasses, clearErrorRegistry } from '@fuzzy-street/errors'; 268 | 269 | // Get all registered error classes 270 | const allErrorClasses = listErrorClasses(); 271 | console.log("Available error types:", allErrorClasses); 272 | 273 | // Retrieve a specific error class by name 274 | const ApiError = getErrorClass("ApiError"); 275 | if (ApiError) { 276 | const error = new ApiError({ 277 | message: "API call failed", 278 | cause: { 279 | statusCode: 500, 280 | endpoint: "/api/users" 281 | } 282 | }); 283 | } 284 | 285 | // For testing: clear the registry 286 | clearErrorRegistry(); 287 | ``` 288 | 289 | ## 📐 API Reference 290 | 291 | ### `createCustomError(name, contextKeys, parentError?)` 292 | 293 | Creates a new custom error class with typed context. 294 | 295 | **Parameters:** 296 | - `name`: `string` - Name for the error class 297 | - `contextKeys`: `(keyof Context)[]` - Register the top-level Keys to determine the exact context for each error class 298 | - `parentError?`: `CustomErrorClass` - Optional parent error class which to inherit context from 299 | 300 | **Returns:** `CustomErrorClass` 301 | 302 | ### `CustomErrorClass` Constructor Options 303 | 304 | ```typescript 305 | { 306 | message: string; // Error message 307 | cause?: Context | string; // Context object or cause message 308 | parent?: Error; // Parent error reference 309 | captureStack?: boolean; // Whether to capture stack trace (default: true) 310 | enumerableProperties?: boolean | string[]; // Make properties enumerable (default: false) 311 | collisionStrategy?: 'override' | 'preserve' | 'error'; // How to handle property collisions 312 | maxParentChainLength?: number; // Max depth for parent chain traversal 313 | } 314 | ``` 315 | 316 | ### `CustomErrorClass` Static Methods 317 | 318 | These methods are provided to help provide better debugging and diagnostic support to us, when we are consuming `CustomErrorClasses` in the wild. 319 | 320 | #### `.getContext(error, options?)` 321 | 322 | Retrieves the context associated with an error. Do bear in-mind that the **context** is the *contextual* information that was passed to each error `cause`. This would always be available to you on the presence of each *`createdCustomError`* 323 | 324 | **Parameters:** 325 | - `error`: `unknown` - The error to examine 326 | - `options?.includeParentContext?`: `boolean` - Whether to include parent context (default: true) 327 | 328 | **Returns:** `Context | undefined` 329 | 330 | #### `.getErrorHierarchy(error)` 331 | 332 | Gets the full error hierarchy information including contexts. 333 | 334 | **Parameters:** 335 | - `error`: `unknown` - The error to analyze 336 | 337 | **Returns:** `CustomErrorHierarchyItem[]` 338 | 339 | #### `.followParentChain(error, options?)` 340 | 341 | Follows and returns the entire chain of parent errors. 342 | 343 | **Parameters:** 344 | - `error`: `Error & { parent?: Error }` - The starting error 345 | - `options?.maxDepth?`: `number` - Maximum depth to traverse (default: 100) 346 | 347 | **Returns:** `Error[]` 348 | 349 | #### `.getInstances()` 350 | 351 | Returns the complete inheritance chain of error classes. 352 | 353 | **Returns:** `CustomErrorClass[]` 354 | 355 | #### `.createFast(message, context?)` 356 | 357 | Creates an error instance with minimal overhead for extremely high-performance scenarios and workloads. 358 | 359 | **Parameters:** 360 | - `message`: `string` - Error message 361 | - `context?`: `Partial` - Optional context object 362 | 363 | **Returns:** `Error & Context` 364 | 365 | ### `isError(error, instance)` 366 | 367 | Type-safe instance checking with proper TypeScript inference. 368 | 369 | **Parameters:** 370 | - `error`: `unknown` - The error to check 371 | - `instance`: `CustomErrorClass` - The error class to check against 372 | 373 | **Returns:** `error is (Error & T)` - Type guard assertion 374 | 375 | ### `getErrorClass(name)` 376 | 377 | Retrieves a registered error class by name. 378 | 379 | **Parameters:** 380 | - `name`: `string` - The name of the error class 381 | 382 | **Returns:** `CustomErrorClass | undefined` 383 | 384 | ### `listErrorClasses()` 385 | 386 | Lists all registered error class names. 387 | 388 | **Returns:** `string[]` 389 | 390 | ### `clearErrorRegistry()` 391 | 392 | Clears all registered error classes (useful for testing). 393 | 394 | ### `Error` Instance Properties 395 | 396 | - `.name`: `string` - The name of the error 397 | - `.message`: `string` - The error message 398 | - `.parent?`: `Error` - Reference to the parent error, if any 399 | - `.inheritanceChain?`: `CustomErrorClass[]` - Array of parent error classes 400 | - `[contextKeys]` - Direct access to all context properties with full type safety 401 | 402 | ## 🔄 Error Inheritance vs. Parent Relationships 403 | 404 | This library supports two distinct concepts that are often confused: 405 | 406 | 1. **Class Inheritance** - The `createCustomError` function allows creating error classes that inherit from other error classes, establishing an *is-a* relationship. 407 | 408 | 2. **Parent-Child Relationship** - Instances of errors can have a parent-child relationship, where one error caused another, establishing a *caused-by* relationship. 409 | 410 | Example: 411 | ```typescript 412 | // Class inheritance (NetworkError is-a ApiError) 413 | const NetworkError = createCustomError<{}, typeof ApiError>( 414 | "NetworkError", [], ApiError 415 | ); 416 | 417 | // Parent-child relationship (apiError caused-by networkError) 418 | const networkError = new NetworkError({...}); 419 | const apiError = new ApiError({ 420 | message: "API call failed", 421 | parent: networkError // networkError is the parent of apiError 422 | }); 423 | ``` 424 | 425 | ## 🌟 Advanced Usage 426 | 427 | ### Handling Context Property Collisions 428 | 429 | ```typescript 430 | // Define error with collision detection 431 | const UserError = createCustomError<{ 432 | name: string; // This would collide with Error.name 433 | }>("UserError", ["name"]); 434 | 435 | // This will throw an error about property collision 436 | try { 437 | new UserError({ 438 | message: "User error", 439 | cause: { name: "John" }, 440 | collisionStrategy: 'error' // Will throw if collision detected 441 | }); 442 | } catch (e) { 443 | console.log(e.message); // "Context property 'name' conflicts with a standard Error property" 444 | } 445 | 446 | // Using override strategy (default) 447 | const error = new UserError({ 448 | message: "User error", 449 | cause: { name: "John" }, 450 | collisionStrategy: 'override' // Will override the built-in property 451 | }); 452 | ``` 453 | 454 | ### Dynamic Error Creation 455 | 456 | ```typescript 457 | function createDomainErrors(domain: string) { 458 | const BaseDomainError = createCustomError<{ 459 | domain: string; 460 | correlationId: string; 461 | }>(`${domain}Error`, ["domain", "correlationId"]); 462 | 463 | const ValidationError = createCustomError<{ 464 | field: string; 465 | value: unknown; 466 | }, typeof BaseDomainError>( 467 | `${domain}ValidationError`, 468 | ["field", "value"], 469 | BaseDomainError 470 | ); 471 | 472 | return { 473 | BaseDomainError, 474 | ValidationError 475 | }; 476 | } 477 | 478 | // Create domain-specific errors 479 | const { BaseDomainError, ValidationError } = createDomainErrors("User"); 480 | const { ValidationError: ProductValidationError } = createDomainErrors("Product"); 481 | 482 | // Usage 483 | throw new ValidationError({ 484 | message: "Invalid user data", 485 | cause: { 486 | domain: "User", 487 | correlationId: "abc-123", 488 | field: "email", 489 | value: "not-an-email" 490 | } 491 | }); 492 | ``` 493 | 494 | ### Error Factory Functions 495 | 496 | ```typescript 497 | function createApiError(endpoint: string, statusCode: number, details: string) { 498 | return new ApiError({ 499 | message: `API Error: ${details}`, 500 | cause: { 501 | endpoint, 502 | statusCode, 503 | timestamp: new Date().toISOString() 504 | } 505 | }); 506 | } 507 | 508 | // For high-frequency scenarios, use createFast 509 | function createApiErrorFast(endpoint: string, statusCode: number) { 510 | return ApiError.createFast(`API Error (${statusCode})`, { 511 | endpoint, 512 | statusCode, 513 | timestamp: new Date().toISOString() 514 | }); 515 | } 516 | 517 | // Usage 518 | throw createApiError("/users", 404, "User not found"); 519 | ``` 520 | 521 | ### Circular Reference Protection 522 | 523 | ```typescript 524 | // Create error types 525 | const ServiceError = createCustomError<{ service: string }>( 526 | "ServiceError", ["service"] 527 | ); 528 | const DependencyError = createCustomError<{ dependency: string }>( 529 | "DependencyError", ["dependency"] 530 | ); 531 | 532 | // Create circular reference (normally happens in complex systems) 533 | const service1Error = new ServiceError({ 534 | message: "Service 1 failed", 535 | cause: { service: "service1" } 536 | }); 537 | 538 | const service2Error = new ServiceError({ 539 | message: "Service 2 failed", 540 | cause: { service: "service2" } 541 | }); 542 | 543 | // Create circular reference 544 | service1Error.parent = service2Error; 545 | service2Error.parent = service1Error; 546 | 547 | // Safe traversal without infinite recursion 548 | const chain = ServiceError.followParentChain(service1Error); 549 | console.log(`Chain length: ${chain.length}`); // Will be 2, not infinite 550 | 551 | // Same protection in hierarchy analysis 552 | const hierarchy = ServiceError.getErrorHierarchy(service1Error); 553 | console.log(`Hierarchy items: ${hierarchy.length}`); // Also stops at circular reference 554 | ``` 555 | 556 | ## 🧪 Running the Examples 557 | 558 | We have code that includes comprehensive [examples](src/examples.ts) that demonstrate the full range of capabilities for this wee this library. Clone the repo, run them locally to see the error hierarchies in action: 559 | 560 | ```bash 561 | # From the root of the project 562 | pnpm run examples 563 | ``` 564 | 565 | ## 🤝 Contributing 566 | 567 | Your **Contributions are always welcome**. Please feel free to submit a Pull Request or even an Issue, its entirely up to you. 568 | 569 | Remember we all stand on the shoulders of giants, 570 | 571 | 💚 572 | 573 | ## 📜 License 574 | 575 | MIT -------------------------------------------------------------------------------- /biome.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://biomejs.dev/schemas/1.9.4/schema.json", 3 | "vcs": { 4 | "enabled": false, 5 | "clientKind": "git", 6 | "useIgnoreFile": true 7 | }, 8 | "files": { 9 | "ignoreUnknown": false, 10 | "ignore": ["node_modules", "dist", "coverage"], 11 | "include": ["src"] 12 | }, 13 | "formatter": { 14 | "enabled": true, 15 | "indentStyle": "space", 16 | "indentWidth": 2, 17 | "formatWithErrors": true, 18 | "lineWidth": 100 19 | }, 20 | "organizeImports": { 21 | "enabled": true 22 | }, 23 | "linter": { 24 | "enabled": true, 25 | "rules": { 26 | "recommended": true, 27 | "suspicious": { 28 | "noExplicitAny": "off" 29 | }, 30 | "complexity": { 31 | "noBannedTypes": "off" 32 | } 33 | } 34 | }, 35 | "javascript": { 36 | "formatter": { 37 | "quoteStyle": "double" 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /mise.toml: -------------------------------------------------------------------------------- 1 | [tools] 2 | node = "lts" 3 | pnpm = "latest" 4 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@fuzzy-street/errors", 3 | "version": "1.1.0", 4 | "description": "Type-safe custom error classes with full context support", 5 | "main": "dist/main.js", 6 | "module": "dist/main.mjs", 7 | "types": "dist/main.d.ts", 8 | "type": "module", 9 | "files": [ 10 | "dist", 11 | "README.md", 12 | "LICENSE" 13 | ], 14 | "keywords": [ 15 | "error", 16 | "typescript", 17 | "context" 18 | ], 19 | "author": "aFuzzyBear", 20 | "license": "MIT", 21 | "packageManager": "pnpm@10.7.1", 22 | "scripts": { 23 | "dev": "tsx --watch --no-cache src/main.ts", 24 | "build": "tsup", 25 | "examples": "tsx --no-cache src/examples.ts", 26 | "test": "node --import tsx --experimental-test-coverage --test-reporter=spec --test \"**/*.test.{ts,js}\"", 27 | "test:ci": "node --import tsx --experimental-test-coverage --test-reporter=spec --test \"**/*.test.{ts,js}\" || echo 'No test files found or tests failed'", 28 | "test:watch": "node --import tsx --test-reporter=spec --test --watch \"**/*.test.{ts,js}\"", 29 | "check": "tsc --noEmit", 30 | "lint": "biome lint --fix", 31 | "format": "biome format --fix --write", 32 | "setup-hooks": "node setup-git-hooks.js", 33 | "release": "standard-version", 34 | "release:minor": "standard-version --release-as minor", 35 | "release:patch": "standard-version --release-as patch", 36 | "release:major": "standard-version --release-as major", 37 | "cut-release": "git push --follow-tags origin main" 38 | }, 39 | "devDependencies": { 40 | "@biomejs/biome": "^1.9.4", 41 | "@commitlint/cli": "^19.8.0", 42 | "@commitlint/config-conventional": "^19.8.0", 43 | "@types/node": "^22.14.0", 44 | "lint-staged": "^15.5.0", 45 | "standard-version": "^9.5.0", 46 | "tsup": "^8.4.0", 47 | "tsx": "^4.19.3", 48 | "typescript": "^5.8.2" 49 | }, 50 | "directories": { 51 | "doc": "docs" 52 | }, 53 | "repository": { 54 | "type": "git", 55 | "url": "git+https://github.com/fuzzy-st/errors.git" 56 | }, 57 | "bugs": { 58 | "url": "https://github.com/fuzzy-st/errors/issues" 59 | }, 60 | "homepage": "https://github.com/fuzzy-st/errors#readme" 61 | } 62 | -------------------------------------------------------------------------------- /pnpm-workspace.yaml: -------------------------------------------------------------------------------- 1 | onlyBuiltDependencies: 2 | - '@biomejs/biome' 3 | - esbuild 4 | -------------------------------------------------------------------------------- /setup-git-hooks.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | import { execSync } from 'child_process'; 3 | import fs from 'fs'; 4 | 5 | // Check if .git-hooks directory exists 6 | if (!fs.existsSync('.git-hooks')) { 7 | console.error('Error: .git-hooks directory not found. Please run this script from the project root.'); 8 | process.exit(1); 9 | } 10 | 11 | // Configure git to use our hooks 12 | try { 13 | execSync('git config core.hooksPath .git-hooks'); 14 | console.log('Git hooks configured successfully! ✅'); 15 | } catch (error) { 16 | console.error('Error configuring git hooks:', error.message); 17 | process.exit(1); 18 | } 19 | -------------------------------------------------------------------------------- /src/examples.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Enhanced Error Hierarchy - Examples 3 | * 4 | * This file contains comprehensive examples demonstrating the usage of the 5 | * Enhanced Error Hierarchy library. Each example is clearly documented 6 | * and represents a common use case or pattern. 7 | */ 8 | 9 | import { createCustomError, isError } from "./main"; 10 | 11 | /** 12 | * EXAMPLE 1: Basic Error Creation 13 | * ============================== 14 | * Demonstrates the most basic usage pattern of creating and throwing 15 | * custom errors with context. 16 | */ 17 | 18 | /** 19 | * @example 1.1 - Simple Error Creation 20 | * Create a basic error class with typed context. 21 | */ 22 | function example1_1() { 23 | // Create a basic error class 24 | const SimpleError = createCustomError<{ 25 | code: number; 26 | detail: string; 27 | }>("SimpleError", ["code", "detail"]); 28 | 29 | // Create and throw an instance 30 | try { 31 | throw new SimpleError({ 32 | message: "A simple error occurred", 33 | cause: { 34 | code: 400, 35 | detail: "Bad Request", 36 | }, 37 | captureStack: true, 38 | }); 39 | } catch (error) { 40 | // Use `checkInstance` for proper TypeScript type inference 41 | if (isError(error, SimpleError)) { 42 | console.log("EXAMPLE 1.1: Simple Error"); 43 | console.log(error); 44 | console.log(error.toString()); 45 | 46 | // Directly access context properties with full TypeScript support 47 | console.log(`Error code: ${error.code}`); 48 | console.log(`Error detail: ${error.detail}`); 49 | 50 | // Access via context getter also available 51 | console.log("Context:", SimpleError.getContext(error)); 52 | console.log("\n"); 53 | } 54 | } 55 | } 56 | 57 | /** 58 | * @example 1.2 - API Error 59 | * Create an API-specific error with relevant context. 60 | */ 61 | function example1_2() { 62 | const ApiError = createCustomError<{ 63 | statusCode: number; 64 | endpoint: string; 65 | responseBody?: string; 66 | }>("ApiError", ["statusCode", "endpoint", "responseBody"]); 67 | 68 | try { 69 | throw new ApiError({ 70 | message: "Failed to fetch data from API", 71 | cause: { 72 | statusCode: 404, 73 | endpoint: "/api/users", 74 | responseBody: JSON.stringify({ error: "Resource not found" }), 75 | }, 76 | }); 77 | } catch (error) { 78 | if (isError(error, ApiError)) { 79 | console.log("EXAMPLE 1.2: API Error"); 80 | console.log(error.toString()); 81 | 82 | // Direct property access with TypeScript support 83 | console.log(`Failed with status ${error.statusCode} on ${error.endpoint}`); 84 | 85 | // Parse the response body if available 86 | if (error.responseBody) { 87 | try { 88 | const response = JSON.parse(error.responseBody); 89 | console.log(`Error details: ${response.error}`); 90 | } catch (e) { 91 | console.log("Could not parse response body"); 92 | } 93 | } 94 | } 95 | } 96 | console.log("\n"); 97 | } 98 | 99 | /** 100 | * EXAMPLE 2: Error Hierarchies 101 | * =========================== 102 | * Demonstrates creating hierarchical error structures where child errors 103 | * inherit from parent errors, with proper context inheritance. 104 | */ 105 | 106 | /** 107 | * @example 2.1 - Basic Error Hierarchy 108 | * Create a simple two-level error hierarchy. 109 | */ 110 | function example2_1() { 111 | // Base error 112 | const BaseError = createCustomError<{ 113 | timestamp: string; 114 | severity: "low" | "medium" | "high"; 115 | }>("BaseError", ["timestamp", "severity"]); 116 | 117 | // Specialized error 118 | const DataError = createCustomError< 119 | { 120 | dataSource: string; 121 | dataType: string; 122 | }, 123 | typeof BaseError 124 | >("DataError", ["dataSource", "dataType"], BaseError); 125 | 126 | try { 127 | throw new DataError({ 128 | message: "Failed to process data", 129 | cause: { 130 | // DataError specific context 131 | dataSource: "database", 132 | dataType: "user", 133 | 134 | // BaseError context 135 | timestamp: new Date().toISOString(), 136 | severity: "high", 137 | }, 138 | }); 139 | } catch (error) { 140 | if (isError(error, DataError)) { 141 | console.log("EXAMPLE 2.1: Basic Error Hierarchy"); 142 | console.log("Example of Error call:\n", error); 143 | console.log("Example of Error Serialized:\n", error.toString()); 144 | 145 | // Direct access to all properties 146 | console.log(`Data Source: ${error.dataSource}`); 147 | console.log(`Data Type: ${error.dataType}`); 148 | console.log(`Timestamp: ${error.timestamp}`); 149 | console.log(`Severity: ${error.severity}`); 150 | 151 | // Full context (includes BaseError context) 152 | const fullContext = DataError.getContext(error); 153 | console.log("Full context:", fullContext); 154 | 155 | // Just DataError context 156 | const dataContext = DataError.getContext(error, { 157 | includeParentContext: false, // Filter out parent context 158 | }); 159 | console.log("Data context only:", dataContext); 160 | 161 | console.log("\n"); 162 | } 163 | } 164 | } 165 | 166 | /** 167 | * @example 2.2 - Three-Level Error Hierarchy 168 | * Create a three-level error hierarchy with context at each level. 169 | */ 170 | function example2_2() { 171 | // Application error - base level 172 | const AppError = createCustomError<{ 173 | appName: string; 174 | version: string; 175 | }>("AppError", ["appName", "version"]); 176 | 177 | // Database error - mid level 178 | const DatabaseError = createCustomError< 179 | { 180 | dbName: string; 181 | query: string; 182 | }, 183 | typeof AppError 184 | >("DatabaseError", ["dbName", "query"], AppError); 185 | 186 | // Query error - leaf level 187 | const QueryError = createCustomError< 188 | { 189 | errorCode: string; 190 | affectedRows: number; 191 | }, 192 | typeof DatabaseError 193 | >("QueryError", ["errorCode", "affectedRows"], DatabaseError); 194 | 195 | try { 196 | throw new QueryError({ 197 | message: "Failed to execute database query", 198 | cause: { 199 | // QueryError specific context 200 | errorCode: "ER_DUP_ENTRY", 201 | affectedRows: 0, 202 | 203 | // DatabaseError context 204 | dbName: "customers", 205 | query: "INSERT INTO users (email) VALUES ('existing@example.com')", 206 | 207 | // AppError context 208 | appName: "CustomerManagement", 209 | version: "1.0.0", 210 | }, 211 | captureStack: true, 212 | }); 213 | } catch (error) { 214 | if (isError(error, QueryError)) { 215 | console.log("EXAMPLE 2.2: Three-Level Error Hierarchy"); 216 | console.log(error.toString()); 217 | 218 | // Access properties directly across the inheritance hierarchy 219 | console.log(`Error Code: ${error.errorCode}`); 220 | console.log(`Database: ${error.dbName}`); 221 | console.log(`Query: ${error.query}`); 222 | console.log(`Application: ${error.appName}`); 223 | console.log(`Version: ${error.version}`); 224 | 225 | // Get error hierarchy information 226 | const hierarchy = QueryError.getErrorHierarchy(error); 227 | console.log("Error hierarchy:", JSON.stringify(hierarchy, null, 2)); 228 | 229 | // Get inheritance chain 230 | console.log( 231 | "Inheritance chain:", 232 | QueryError.followParentChain(error).map((e) => e.name), 233 | ); 234 | 235 | console.log("\n"); 236 | } 237 | } 238 | } 239 | 240 | /** 241 | * EXAMPLE 3: Parent-Child Relationships 242 | * ==================================== 243 | * Demonstrates creating parent-child relationships between error instances, 244 | * which is different from class inheritance. 245 | */ 246 | 247 | /** 248 | * @example 3.1 - Basic Parent-Child Relationship 249 | * Create a simple parent-child relationship between errors. 250 | */ 251 | function example3_1() { 252 | const NetworkError = createCustomError<{ 253 | hostname: string; 254 | port: number; 255 | }>("NetworkError", ["hostname", "port"]); 256 | 257 | const ServiceError = createCustomError<{ 258 | serviceName: string; 259 | operation: string; 260 | }>("ServiceError", ["serviceName", "operation"]); 261 | 262 | try { 263 | try { 264 | // This is the parent error (cause) 265 | throw new NetworkError({ 266 | message: "Failed to connect to remote server", 267 | cause: { 268 | hostname: "api.example.com", 269 | port: 443, 270 | }, 271 | }); 272 | } catch (networkError) { 273 | if (isError(networkError, NetworkError)) { 274 | // This is the child error (caused by the network error) 275 | throw new ServiceError({ 276 | message: "Authentication service unavailable", 277 | parent: networkError, // Pass the error in establish parent relationship 278 | captureStack: true, 279 | }); 280 | } 281 | throw networkError; 282 | } 283 | } catch (error) { 284 | if (isError(error, ServiceError)) { 285 | console.log("EXAMPLE 3.1: Basic Parent-Child Relationship"); 286 | console.log(error.toString()); 287 | 288 | // Access parent error 289 | if (isError(error, NetworkError)) { 290 | console.log(`Parent error context: Failed to connect to ${error.hostname}:${error.port}`); 291 | } 292 | 293 | // Follow the parent chain 294 | const chain = ServiceError.followParentChain(error); 295 | console.log(`Error chain length: ${chain.length}`); 296 | chain.forEach((err, index) => { 297 | console.log(`Chain[${index}]:`, err.name, "-", err.message); 298 | }); 299 | 300 | console.log("\n"); 301 | } 302 | } 303 | } 304 | 305 | /** 306 | * @example 3.2 - Multi-level Error Chain 307 | * Create a chain of errors with multiple levels. 308 | */ 309 | function example3_2() { 310 | const SystemError = createCustomError<{ 311 | component: string; 312 | }>("SystemError", ["component"]); 313 | 314 | const FileError = createCustomError<{ 315 | path: string; 316 | operation: string; 317 | }>("FileError", ["path", "operation"]); 318 | 319 | const ConfigError = createCustomError<{ 320 | configKey: string; 321 | expectedType: string; 322 | }>("ConfigError", ["configKey", "expectedType"]); 323 | 324 | try { 325 | try { 326 | try { 327 | // Level 3 (root cause) 328 | throw new SystemError({ 329 | message: "File system unavailable", 330 | cause: { 331 | component: "disk_controller", 332 | }, 333 | }); 334 | } catch (systemError) { 335 | if (isError(systemError, SystemError)) { 336 | // Level 2 337 | throw new FileError({ 338 | message: "Could not read configuration file", 339 | parent: systemError, // Parent relationship 340 | captureStack: true, 341 | }); 342 | } 343 | throw systemError; 344 | } 345 | } catch (fileError) { 346 | if (isError(fileError, FileError)) { 347 | // Level 1 (what the application code catches) 348 | throw new ConfigError({ 349 | message: "Application configuration invalid", 350 | cause: { 351 | configKey: "AK47", 352 | expectedType: "string", 353 | }, // Custom context 354 | parent: fileError, // Parent relationship 355 | captureStack: true, 356 | }); 357 | } 358 | throw fileError; 359 | } 360 | } catch (error) { 361 | console.log("EXAMPLE 3.2: Multi-level Error Chain"); 362 | if (isError(error, ConfigError)) { 363 | // Access properties from the error 364 | console.log(`Config error key: ${error.configKey || "N/A"}`); 365 | console.log(`Expected type: ${error.expectedType || "N/A"}`); 366 | } 367 | // Check and access the parent if it exists 368 | if (isError(error, FileError)) { 369 | const fileErrorContext = FileError.getContext(error); 370 | console.log(`File error path: ${fileErrorContext?.path || "N/A"}`); 371 | console.log(`File operation: ${fileErrorContext?.operation || "N/A"}`); 372 | } 373 | // Check and access the grandparent if it exists 374 | if (isError(error, SystemError)) { 375 | const systemErrorContext = SystemError.getContext(error); 376 | console.log(`System component: ${systemErrorContext?.component}`); 377 | } 378 | 379 | // Follow complete error chain 380 | //@ts-expect-error - Trust me its a CustomError 381 | const errorChain = ConfigError.followParentChain(error); 382 | console.log(`Complete error chain (${errorChain.length} errors):`); 383 | 384 | errorChain.forEach((err, index) => { 385 | console.log(`[${index}] ${err.name}: ${err.message}`); 386 | }); 387 | 388 | // Get full error hierarchy with contexts 389 | const hierarchy = ConfigError.getErrorHierarchy(error); 390 | console.log("Full error hierarchy:", JSON.stringify(hierarchy, null, 2)); 391 | 392 | console.log("\n"); 393 | } 394 | } 395 | 396 | /** 397 | * EXAMPLE 4: Mixed Inheritance and Parent Relationships 398 | * =================================================== 399 | * Demonstrates combining class inheritance hierarchies with 400 | * instance parent-child relationships. 401 | */ 402 | 403 | /** 404 | * @example 4.1 - Combined Inheritance and Parent Chain 405 | * Use both inheritance and parent relationships together. 406 | */ 407 | function example4_1() { 408 | // Class inheritance (level 1) 409 | const BaseError = createCustomError<{ 410 | application: string; 411 | }>("BaseError", ["application"]); 412 | 413 | // Class inheritance (level 2) 414 | const DatabaseError = createCustomError< 415 | { 416 | database: string; 417 | }, 418 | typeof BaseError 419 | >("DatabaseError", ["database"], BaseError); 420 | 421 | // Class inheritance (level 3) 422 | const QueryError = createCustomError< 423 | { 424 | query: string; 425 | }, 426 | typeof DatabaseError 427 | >("QueryError", ["query"], DatabaseError); 428 | 429 | // Independent error for parent chain 430 | const NetworkError = createCustomError<{ 431 | host: string; 432 | }>("NetworkError", ["host"]); 433 | 434 | try { 435 | // Create parent error 436 | const netError = new NetworkError({ 437 | message: "Network connection failed", 438 | cause: { 439 | host: "db.example.com", 440 | }, 441 | }); 442 | 443 | // Create child error with inherited context from class hierarchy 444 | // and parent-child relationship to the NetworkError 445 | throw new QueryError({ 446 | message: "Failed to execute query due to connection issue", 447 | cause: { 448 | // QueryError context 449 | query: "SELECT * FROM users", 450 | 451 | // DatabaseError context 452 | database: "main_users", 453 | 454 | // BaseError context 455 | application: "UserService", 456 | }, 457 | overridePrototype: DatabaseError, // Explicit class inheritance 458 | captureStack: true, 459 | }); 460 | } catch (error) { 461 | if (isError(error, QueryError)) { 462 | console.log("EXAMPLE 4.1: Combined Inheritance and Parent Chain"); 463 | 464 | // Access properties directly across the inheritance chain 465 | console.log(`Query: ${error.query}`); 466 | console.log(`Database: ${error.database}`); 467 | console.log(`Application: ${error.application}`); 468 | 469 | // Inspect the inheritance chain (class hierarchy) 470 | console.log( 471 | "Class inheritance chain:", 472 | QueryError.followParentChain(error) 473 | ?.map((e) => e.name) 474 | .join(" > "), 475 | ); 476 | 477 | // Get full context (from all levels of inheritance) 478 | const context = QueryError.getContext(error); 479 | console.log(`Full ${error.name} context from inheritance:`, context); 480 | 481 | console.log("\n"); 482 | } 483 | 484 | // Check if we have a NetworkError 485 | const netError = new NetworkError({ 486 | message: "Network example", 487 | cause: { host: "example.com" }, 488 | }); 489 | 490 | if (isError(netError, NetworkError)) { 491 | console.log("Network error host:", netError.host); 492 | } 493 | } 494 | } 495 | 496 | /** 497 | * EXAMPLE 5: Advanced Usage Patterns 498 | * ================================ 499 | * Demonstrates more advanced usage patterns and techniques. 500 | */ 501 | 502 | /** 503 | * @example 5.1 - Dynamic Error Creation 504 | * Create error classes dynamically based on domain. 505 | */ 506 | function example5_1() { 507 | // Factory function to create domain-specific errors 508 | function createDomainErrors(domain: string) { 509 | const BaseDomainError = createCustomError<{ 510 | domain: string; 511 | correlationId: string; 512 | }>(`${domain}Error`, ["domain", "correlationId"]); 513 | 514 | const ValidationError = createCustomError< 515 | { 516 | field: string; 517 | value: unknown; 518 | }, 519 | typeof BaseDomainError 520 | >(`${domain}ValidationError`, ["field", "value"], BaseDomainError); 521 | 522 | const ProcessingError = createCustomError< 523 | { 524 | process: string; 525 | step: string; 526 | }, 527 | typeof BaseDomainError 528 | >(`${domain}ProcessingError`, ["process", "step"], BaseDomainError); 529 | 530 | return { 531 | BaseDomainError, 532 | ValidationError, 533 | ProcessingError, 534 | }; 535 | } 536 | 537 | // Create user domain errors 538 | const UserErrors = createDomainErrors("User"); 539 | 540 | try { 541 | throw new UserErrors.ValidationError({ 542 | message: "Invalid user data provided", 543 | cause: { 544 | // ValidationError context 545 | field: "email", 546 | value: "not-an-email", 547 | 548 | // BaseDomainError context 549 | domain: "User", 550 | correlationId: "usr-123-456-789", 551 | }, 552 | }); 553 | } catch (error) { 554 | if (error instanceof Error) { 555 | console.log("EXAMPLE 5.1: Dynamic Error Creation"); 556 | console.log(`Error type: ${error.name}`); 557 | console.log(`Error message: ${error.message}`); 558 | console.log(error.toString()); 559 | } 560 | 561 | // Use checkInstance for proper type inference with dynamically created errors 562 | if (isError(error, UserErrors.ValidationError)) { 563 | console.log(`Validation error on field ${error.field}: ${error.value}`); 564 | console.log(`Domain: ${error.domain}, Correlation ID: ${error.correlationId}`); 565 | } 566 | } 567 | console.log("\n"); 568 | } 569 | 570 | /** 571 | * @example 5.2 - Error Factory Functions 572 | * Create utility functions to generate specific errors. 573 | */ 574 | function example5_2() { 575 | // Define base error types 576 | const ApiError = createCustomError<{ 577 | endpoint: string; 578 | statusCode: number; 579 | timestamp: string; 580 | }>("ApiError", ["endpoint", "statusCode", "timestamp"]); 581 | 582 | // Factory function for creating user-related API errors 583 | function createUserApiError( 584 | statusCode: number, 585 | endpoint: string, 586 | userId?: string, 587 | action?: string, 588 | ) { 589 | const baseMessage = `User API error (${statusCode})`; 590 | const detailedMessage = userId 591 | ? `${baseMessage}: Failed to ${action || "process"} user ${userId}` 592 | : baseMessage; 593 | 594 | return new ApiError({ 595 | message: detailedMessage, 596 | cause: { 597 | endpoint, 598 | statusCode, 599 | timestamp: new Date().toISOString(), 600 | }, 601 | captureStack: true, 602 | }); 603 | } 604 | 605 | try { 606 | // Use the factory function 607 | throw createUserApiError(404, "/api/users/123", "123", "fetch"); 608 | } catch (error) { 609 | if (isError(error, ApiError)) { 610 | console.log("EXAMPLE 5.2: Error Factory Functions"); 611 | console.log(error.toString()); 612 | 613 | // Direct access to properties with TypeScript support 614 | console.log( 615 | `API error details: ${error.statusCode} on ${error.endpoint} at ${error.timestamp}`, 616 | ); 617 | 618 | console.log("\n"); 619 | } 620 | } 621 | } 622 | 623 | /** 624 | * @example 5.3 - Deep Nested Context 625 | * Demonstrate handling of deeply nested context objects. 626 | */ 627 | function example5_3() { 628 | // Error with deeply nested context structure 629 | const ConfigurationError = createCustomError<{ 630 | config: { 631 | server: { 632 | host: string; 633 | port: number; 634 | ssl: { 635 | enabled: boolean; 636 | cert?: string; 637 | }; 638 | }; 639 | database: { 640 | connection: { 641 | host: string; 642 | credentials: { 643 | username: string; 644 | encrypted: boolean; 645 | }; 646 | }; 647 | }; 648 | }; 649 | }>("ConfigurationError", ["config"]); 650 | 651 | try { 652 | throw new ConfigurationError({ 653 | message: "Invalid server configuration", 654 | cause: { 655 | config: { 656 | server: { 657 | host: "localhost", 658 | port: 8080, 659 | ssl: { 660 | enabled: true, 661 | cert: undefined, // Missing certificate 662 | }, 663 | }, 664 | database: { 665 | connection: { 666 | host: "db.example.com", 667 | credentials: { 668 | username: "app_user", 669 | encrypted: false, // Unencrypted credentials 670 | }, 671 | }, 672 | }, 673 | }, 674 | }, 675 | }); 676 | } catch (error) { 677 | if (isError(error, ConfigurationError)) { 678 | console.log("EXAMPLE 5.3: Deep Nested Context"); 679 | 680 | // Direct access to nested properties 681 | const sslEnabled = error.config.server.ssl.enabled; 682 | const hasCert = !!error.config.server.ssl.cert; 683 | const credentialsEncrypted = error.config.database.connection.credentials.encrypted; 684 | 685 | console.log(`SSL Enabled: ${sslEnabled}, Has Cert: ${hasCert}`); 686 | console.log(`Database Credentials Encrypted: ${credentialsEncrypted}`); 687 | 688 | if (sslEnabled && !hasCert) { 689 | console.log("ERROR: SSL is enabled but no certificate is provided"); 690 | } 691 | 692 | if (!credentialsEncrypted) { 693 | console.log("WARNING: Database credentials are not encrypted"); 694 | } 695 | 696 | console.log("\n"); 697 | } 698 | } 699 | } 700 | 701 | /** 702 | * EXAMPLE 6: Real-World Scenarios 703 | * ============================= 704 | * Demonstrates realistic error handling scenarios that might occur in 705 | * production applications. 706 | */ 707 | 708 | /** 709 | * @example 6.1 - Authentication Flow Errors 710 | * Simulate an authentication flow with multiple potential error points. 711 | */ 712 | function example6_1() { 713 | // Define error hierarchy for auth flow 714 | const AuthError = createCustomError<{ 715 | userId?: string; 716 | requestId: string; 717 | }>("AuthError", ["userId", "requestId"]); 718 | 719 | const CredentialsError = createCustomError< 720 | { 721 | reason: "invalid" | "expired" | "locked"; 722 | attemptCount: number; 723 | }, 724 | typeof AuthError 725 | >("CredentialsError", ["reason", "attemptCount"], AuthError); 726 | 727 | const MfaError = createCustomError< 728 | { 729 | mfaType: "sms" | "app" | "email"; 730 | remainingAttempts: number; 731 | }, 732 | typeof AuthError 733 | >("MfaError", ["mfaType", "remainingAttempts"], AuthError); 734 | 735 | const SessionError = createCustomError< 736 | { 737 | sessionId: string; 738 | expiryTime: string; 739 | }, 740 | typeof AuthError 741 | >("SessionError", ["sessionId", "expiryTime"], AuthError); 742 | 743 | // Simulate login with various failure points 744 | function simulateLogin( 745 | username: string, 746 | password: string, 747 | mfaCode?: string, 748 | ): { success: boolean; sessionId?: string; error?: Error } { 749 | const requestId = `auth-${Date.now()}`; 750 | 751 | // Step 1: Validate credentials 752 | if (password.length < 8) { 753 | return { 754 | success: false, 755 | error: new CredentialsError({ 756 | message: "Invalid credentials provided", 757 | cause: { 758 | requestId, 759 | userId: username, 760 | reason: "invalid", 761 | attemptCount: 1, 762 | }, 763 | }), 764 | }; 765 | } 766 | 767 | // Step 2: Check MFA if required 768 | if (!mfaCode) { 769 | return { 770 | success: false, 771 | error: new MfaError({ 772 | message: "MFA verification required", 773 | cause: { 774 | requestId, 775 | userId: username, 776 | mfaType: "app", 777 | remainingAttempts: 3, 778 | }, 779 | }), 780 | }; 781 | } 782 | 783 | if (mfaCode !== "123456") { 784 | return { 785 | success: false, 786 | error: new MfaError({ 787 | message: "Invalid MFA code provided", 788 | cause: { 789 | requestId, 790 | userId: username, 791 | mfaType: "app", 792 | remainingAttempts: 2, 793 | }, 794 | }), 795 | }; 796 | } 797 | 798 | // Step 3: Create session 799 | const sessionId = `session-${Date.now()}`; 800 | const expiryTime = new Date(Date.now() + 3600000).toISOString(); 801 | 802 | // Simulate session creation failure 803 | if (username === "problem_user") { 804 | return { 805 | success: false, 806 | error: new SessionError({ 807 | message: "Failed to create user session", 808 | cause: { 809 | requestId, 810 | userId: username, 811 | sessionId, 812 | expiryTime, 813 | }, 814 | }), 815 | }; 816 | } 817 | 818 | // Success case 819 | return { 820 | success: true, 821 | sessionId, 822 | }; 823 | } 824 | 825 | console.log("EXAMPLE 6.1: Authentication Flow Errors"); 826 | 827 | // Scenario 1: Invalid password 828 | const result1 = simulateLogin("user@example.com", "short", "123456"); 829 | if (!result1.success && result1.error) { 830 | console.log("Scenario 1: Invalid password"); 831 | console.log(result1.error.toString()); 832 | 833 | if (isError(result1.error, CredentialsError)) { 834 | // Direct property access with full TypeScript support 835 | console.log(`Auth failed for user: ${result1.error.userId}, reason: ${result1.error.reason}`); 836 | console.log(`Attempt count: ${result1.error.attemptCount}`); 837 | } 838 | } 839 | 840 | // Scenario 2: Missing MFA code 841 | const result2 = simulateLogin("user@example.com", "password123"); 842 | if (!result2.success && result2.error) { 843 | console.log("\nScenario 2: Missing MFA code"); 844 | console.log(result2.error.toString()); 845 | 846 | if (isError(result2.error, MfaError)) { 847 | // Direct property access 848 | console.log( 849 | `MFA required: ${result2.error.mfaType}, remaining attempts: ${result2.error.remainingAttempts}`, 850 | ); 851 | } 852 | } 853 | 854 | // Scenario 3: Session creation error 855 | const result3 = simulateLogin("problem_user", "password123", "123456"); 856 | if (!result3.success && result3.error) { 857 | console.log("\nScenario 3: Session creation error"); 858 | console.log(result3.error.toString()); 859 | 860 | if (isError(result3.error, SessionError)) { 861 | // Direct property access 862 | console.log( 863 | `Session creation failed: ${result3.error.sessionId}, would expire at: ${result3.error.expiryTime}`, 864 | ); 865 | console.log(`User ID: ${result3.error.userId}, Request ID: ${result3.error.requestId}`); 866 | } 867 | } 868 | 869 | // Scenario 4: Successful login 870 | const result4 = simulateLogin("good_user", "password123", "123456"); 871 | if (result4.success) { 872 | console.log("\nScenario 4: Successful login"); 873 | console.log(`Login successful! Session ID: ${result4.sessionId}`); 874 | } 875 | 876 | console.log("\n"); 877 | } 878 | 879 | /** 880 | * Example demonstrating direct context access 881 | */ 882 | function demonstrateDirectContextAccess() { 883 | console.log("\n-------------------------------------"); 884 | console.log("EXAMPLE: Direct Context Access"); 885 | console.log("-------------------------------------"); 886 | 887 | // Create a basic error class with typed context 888 | const ApiError = createCustomError<{ 889 | statusCode: number; 890 | endpoint: string; 891 | responseData?: any; 892 | }>("ApiError", ["statusCode", "endpoint", "responseData"]); 893 | 894 | // Create a derived error class 895 | const NetworkError = createCustomError< 896 | { 897 | retryCount: number; 898 | timeout: number; 899 | }, 900 | typeof ApiError 901 | >("NetworkError", ["retryCount", "timeout"], ApiError); 902 | try { 903 | // Create an error with context 904 | const error = new NetworkError({ 905 | message: "Failed to connect to API server", 906 | cause: { 907 | // NetworkError specific context 908 | retryCount: 3, 909 | timeout: 5000, 910 | 911 | // ApiError inherited context 912 | statusCode: 503, 913 | endpoint: "/api/users", 914 | responseData: { error: "Service Unavailable" }, 915 | }, 916 | captureStack: true, 917 | }); 918 | 919 | throw error; 920 | } catch (error) { 921 | if (isError(error, NetworkError)) { 922 | console.log("Error details:", error.name, "-", error.message); 923 | 924 | // Method 1: Accessing context properties directly on the error 925 | console.log("\nAccessing context properties directly:"); 926 | console.log(`Status Code: ${error.statusCode}`); 927 | console.log(`Endpoint: ${error.endpoint}`); 928 | console.log(`Retry Count: ${error.retryCount}`); 929 | console.log(`Timeout: ${error.timeout}`); 930 | 931 | // Method 2: Using the static getContext method 932 | console.log("\nUsing the static getContext method:"); 933 | const context = NetworkError.getContext(error); 934 | if (context) { 935 | console.log(`Status Code: ${context.statusCode}`); 936 | console.log(`Endpoint: ${context.endpoint}`); 937 | console.log(`Retry Count: ${context.retryCount}`); 938 | console.log(`Timeout: ${context.timeout}`); 939 | } 940 | } 941 | } 942 | } 943 | 944 | /** 945 | * Example demonstrating JSON serialization 946 | */ 947 | function demonstrateJsonSerialization() { 948 | console.log("\n-------------------------------------"); 949 | console.log("EXAMPLE: JSON Serialization"); 950 | console.log("-------------------------------------"); 951 | // Create a basic error class with typed context 952 | const ApiError = createCustomError<{ 953 | statusCode: number; 954 | endpoint: string; 955 | responseData?: any; 956 | }>("ApiError", ["statusCode", "endpoint", "responseData"]); 957 | 958 | // Create a derived error class 959 | const NetworkError = createCustomError< 960 | { 961 | retryCount: number; 962 | timeout: number; 963 | }, 964 | typeof ApiError 965 | >("NetworkError", ["retryCount", "timeout"], ApiError); 966 | try { 967 | // Create a parent error 968 | const parentError = new ApiError({ 969 | message: "API returned an error", 970 | cause: { 971 | statusCode: 400, 972 | endpoint: "/api/auth", 973 | responseData: { error: "Invalid credentials" }, 974 | }, 975 | }); 976 | 977 | // Create a child error with the parent 978 | const childError = new NetworkError({ 979 | message: "Network operation failed", 980 | parent: parentError, 981 | captureStack: true, 982 | }); 983 | 984 | throw childError; 985 | } catch (error) { 986 | if (error instanceof Error) { 987 | console.log("Original error.toString():"); 988 | console.log(error.toString()); 989 | 990 | console.log("\nJSON.stringify() result:"); 991 | const serialized = JSON.stringify(error, null, 2); 992 | console.log(serialized); 993 | 994 | console.log("\nParsed JSON:"); 995 | const parsed = JSON.parse(serialized); 996 | console.log("Error name:", parsed.name); 997 | console.log("Parent name:", parsed.parent?.name); 998 | if (parsed.context) { 999 | console.log("Context:", parsed.context); 1000 | } 1001 | } 1002 | } 1003 | } 1004 | 1005 | /** 1006 | * Example demonstrating a complex error hierarchy with direct property access 1007 | */ 1008 | function demonstrateComplexExample() { 1009 | console.log("\n-------------------------------------"); 1010 | console.log("EXAMPLE: Complex Error Hierarchy"); 1011 | console.log("-------------------------------------"); 1012 | 1013 | // Create a three-level error hierarchy 1014 | const BaseError = createCustomError<{ 1015 | application: string; 1016 | version: `v${number}.${number}.${number}`; 1017 | }>("BaseError", ["application", "version"]); 1018 | 1019 | const DatabaseError = createCustomError< 1020 | { 1021 | database: string; 1022 | query: string; 1023 | }, 1024 | typeof BaseError 1025 | >("DatabaseError", ["database", "query"], BaseError); 1026 | 1027 | const QueryError = createCustomError< 1028 | { 1029 | errorCode: string; 1030 | affectedRows: number; 1031 | }, 1032 | typeof DatabaseError 1033 | >("QueryError", ["errorCode", "affectedRows"], DatabaseError); 1034 | 1035 | try { 1036 | throw new QueryError({ 1037 | message: "Failed to execute database query", 1038 | cause: { 1039 | // QueryError specific context 1040 | errorCode: "ER_DUP_ENTRY", 1041 | affectedRows: 0, 1042 | 1043 | // DatabaseError context 1044 | database: "customers", 1045 | query: "INSERT INTO users (email) VALUES ('existing@example.com')", 1046 | 1047 | // BaseError context 1048 | application: "CustomerManagement", 1049 | version: "v1.0.0", 1050 | }, 1051 | captureStack: true, 1052 | }); 1053 | } catch (error) { 1054 | // Use checkInstance for proper TypeScript type inference 1055 | if (isError(error, QueryError)) { 1056 | console.log("Error:", error.name, "-", error.message); 1057 | 1058 | // Directly access properties at different inheritance levels 1059 | console.log("\nAccessing context properties directly across inheritance:"); 1060 | console.log(`Error Code: ${error.errorCode}`); 1061 | console.log(`Database: ${error.database}`); 1062 | console.log(`Application: ${error.application}`); 1063 | console.log(`Version: ${error.version}`); 1064 | 1065 | // Using context getter 1066 | console.log("\nUsing context getter to access all properties:"); 1067 | const { errorCode, database, application, version, query, affectedRows } = error; 1068 | console.log(`Error Code: ${errorCode}`); 1069 | console.log(`Database: ${database}`); 1070 | console.log(`Query: ${query}`); 1071 | console.log(`Affected Rows: ${affectedRows}`); 1072 | console.log(`Application: ${application}`); 1073 | console.log(`Version: ${version}`); 1074 | 1075 | // JSON serialization 1076 | console.log("\nJSON serialization:"); 1077 | console.log(JSON.stringify(error, null, 2)); 1078 | } 1079 | } 1080 | } 1081 | 1082 | /** 1083 | * Run all examples 1084 | * This function executes all the example functions to demonstrate 1085 | * the various capabilities of the Enhanced Error Hierarchy library. 1086 | */ 1087 | export function runAllExamples() { 1088 | // Example 1: Basic Error Creation 1089 | console.log("===================================="); 1090 | console.log("EXAMPLE GROUP 1: BASIC ERROR CREATION"); 1091 | console.log("====================================\n"); 1092 | example1_1(); 1093 | example1_2(); 1094 | 1095 | // Example 2: Error Hierarchies 1096 | console.log("===================================="); 1097 | console.log("EXAMPLE GROUP 2: ERROR HIERARCHIES"); 1098 | console.log("====================================\n"); 1099 | example2_1(); 1100 | example2_2(); 1101 | 1102 | // Example 3: Parent-Child Relationships 1103 | console.log("===================================="); 1104 | console.log("EXAMPLE GROUP 3: PARENT-CHILD RELATIONSHIPS"); 1105 | console.log("====================================\n"); 1106 | example3_1(); 1107 | example3_2(); 1108 | 1109 | // Example 4: Mixed Inheritance and Parent Relationships 1110 | console.log("===================================="); 1111 | console.log("EXAMPLE GROUP 4: MIXED INHERITANCE AND PARENT RELATIONSHIPS"); 1112 | console.log("====================================\n"); 1113 | example4_1(); 1114 | 1115 | // Example 5: Advanced Usage Patterns 1116 | console.log("===================================="); 1117 | console.log("EXAMPLE GROUP 5: ADVANCED USAGE PATTERNS"); 1118 | console.log("====================================\n"); 1119 | example5_1(); 1120 | example5_2(); 1121 | example5_3(); 1122 | 1123 | // Example 6: Real-World Scenarios 1124 | console.log("===================================="); 1125 | console.log("EXAMPLE GROUP 6: REAL-WORLD SCENARIOS"); 1126 | console.log("====================================\n"); 1127 | example6_1(); 1128 | // example6_2(); 1129 | console.log("===================================="); 1130 | 1131 | console.log("=============================================="); 1132 | console.log("ENHANCED CUSTOM ERROR EXAMPLES"); 1133 | console.log("=============================================="); 1134 | 1135 | demonstrateDirectContextAccess(); 1136 | demonstrateJsonSerialization(); 1137 | demonstrateComplexExample(); 1138 | } 1139 | 1140 | runAllExamples(); 1141 | -------------------------------------------------------------------------------- /src/main.test.ts: -------------------------------------------------------------------------------- 1 | import { test, describe } from "node:test"; 2 | import assert from "node:assert/strict"; 3 | import { createCustomError, isError } from "./main"; 4 | 5 | describe("Basic Error Creation", () => { 6 | test("should create a simple error with typed context", () => { 7 | // Create a basic error class 8 | const SimpleError = createCustomError<{ 9 | code: number; 10 | detail: string; 11 | }>("SimpleError", ["code", "detail"]); 12 | 13 | // Create an instance 14 | const error = new SimpleError({ 15 | message: "A simple error occurred", 16 | cause: { 17 | code: 400, 18 | detail: "Bad Request", 19 | }, 20 | }); 21 | 22 | // Assertions 23 | assert.equal(error.name, "SimpleError"); 24 | assert.equal(error.message, "A simple error occurred"); 25 | assert.equal(error.code, 400); 26 | assert.equal(error.detail, "Bad Request"); 27 | 28 | // Check context access via getter 29 | const context = SimpleError.getContext(error); 30 | assert.deepEqual(context, { code: 400, detail: "Bad Request" }); 31 | 32 | // Check instance checking function 33 | assert.equal(isError(error, SimpleError), true); 34 | assert.equal(error instanceof SimpleError, true); 35 | }); 36 | 37 | test("should create an API error with relevant context", () => { 38 | const ApiError = createCustomError<{ 39 | statusCode: number; 40 | endpoint: string; 41 | responseBody?: string; 42 | }>("ApiError", ["statusCode", "endpoint", "responseBody"]); 43 | 44 | const error = new ApiError({ 45 | message: "Failed to fetch data from API", 46 | cause: { 47 | statusCode: 404, 48 | endpoint: "/api/users", 49 | responseBody: JSON.stringify({ error: "Resource not found" }), 50 | }, 51 | }); 52 | 53 | assert.equal(error.name, "ApiError"); 54 | assert.equal(error.statusCode, 404); 55 | assert.equal(error.endpoint, "/api/users"); 56 | assert.equal(error.responseBody, JSON.stringify({ error: "Resource not found" })); 57 | 58 | // Check JSON parsing from responseBody 59 | const parsedResponse = JSON.parse(error.responseBody); 60 | assert.deepEqual(parsedResponse, { error: "Resource not found" }); 61 | }); 62 | 63 | test("should handle missing optional properties", () => { 64 | const ApiError = createCustomError<{ 65 | statusCode: number; 66 | endpoint: string; 67 | responseBody?: string; 68 | }>("ApiError", ["statusCode", "endpoint", "responseBody"]); 69 | 70 | const error = new ApiError({ 71 | message: "Failed to fetch data from API", 72 | cause: { 73 | statusCode: 404, 74 | endpoint: "/api/users", 75 | // responseBody is deliberately omitted 76 | }, 77 | }); 78 | 79 | assert.equal(error.statusCode, 404); 80 | assert.equal(error.endpoint, "/api/users"); 81 | assert.equal(error.responseBody, undefined); 82 | }); 83 | 84 | test("should capture stack trace when requested", () => { 85 | const StackError = createCustomError<{ code: number }>("StackError", ["code"]); 86 | 87 | const error = new StackError({ 88 | message: "An error with stack trace", 89 | cause: { code: 500 }, 90 | captureStack: true, 91 | }); 92 | 93 | assert.ok(error.stack, "Stack trace should be captured"); 94 | assert.ok( 95 | error.stack.includes("StackError: An error with stack trace"), 96 | "Stack trace should include error name and message", 97 | ); 98 | }); 99 | }); 100 | 101 | describe("Error Hierarchies", () => { 102 | test("should create a two-level error hierarchy with proper inheritance", () => { 103 | // Base error 104 | const BaseError = createCustomError<{ 105 | timestamp: string; 106 | severity: "low" | "medium" | "high"; 107 | }>("BaseError", ["timestamp", "severity"]); 108 | 109 | // Specialized error 110 | const DataError = createCustomError< 111 | { 112 | dataSource: string; 113 | dataType: string; 114 | }, 115 | typeof BaseError 116 | >("DataError", ["dataSource", "dataType"], BaseError); 117 | 118 | const timestamp = new Date().toISOString(); 119 | const error = new DataError({ 120 | message: "Failed to process data", 121 | cause: { 122 | // DataError context 123 | dataSource: "user_database", 124 | dataType: "user_profile", 125 | 126 | // BaseError context 127 | timestamp, 128 | severity: "medium", 129 | }, 130 | }); 131 | 132 | // Check inheritance 133 | assert.equal(error.name, "DataError"); 134 | assert.ok(error instanceof DataError); 135 | assert.ok(error instanceof BaseError); 136 | 137 | // Check direct context access across hierarchy 138 | assert.equal(error.dataSource, "user_database"); 139 | assert.equal(error.dataType, "user_profile"); 140 | assert.equal(error.timestamp, timestamp); 141 | assert.equal(error.severity, "medium"); 142 | 143 | // Test context getters 144 | const fullContext = DataError.getContext(error); 145 | assert.deepEqual(fullContext, { 146 | dataSource: "user_database", 147 | dataType: "user_profile", 148 | timestamp, 149 | severity: "medium", 150 | }); 151 | 152 | // Filter to just DataError context (exclude parent) 153 | const dataContext = DataError.getContext(error, { 154 | includeParentContext: false, 155 | }); 156 | assert.deepEqual(dataContext, { 157 | dataSource: "user_database", 158 | dataType: "user_profile", 159 | }); 160 | }); 161 | 162 | test("should create a three-level error hierarchy with context at each level", () => { 163 | // Application error - base level 164 | const AppError = createCustomError<{ 165 | appName: string; 166 | version: string; 167 | }>("AppError", ["appName", "version"]); 168 | 169 | // Database error - mid level 170 | const DatabaseError = createCustomError< 171 | { 172 | dbName: string; 173 | query: string; 174 | }, 175 | typeof AppError 176 | >("DatabaseError", ["dbName", "query"], AppError); 177 | 178 | // Query error - leaf level 179 | const QueryError = createCustomError< 180 | { 181 | errorCode: string; 182 | affectedRows: number; 183 | }, 184 | typeof DatabaseError 185 | >("QueryError", ["errorCode", "affectedRows"], DatabaseError); 186 | 187 | const error = new QueryError({ 188 | message: "Failed to execute database query", 189 | cause: { 190 | // QueryError specific context 191 | errorCode: "ER_DUP_ENTRY", 192 | affectedRows: 0, 193 | 194 | // DatabaseError context 195 | dbName: "customers", 196 | query: "INSERT INTO users (email) VALUES ('existing@example.com')", 197 | 198 | // AppError context 199 | appName: "CustomerManagement", 200 | version: "1.0.0", 201 | }, 202 | }); 203 | 204 | // Check inheritance chain 205 | assert.ok(error instanceof QueryError); 206 | assert.ok(error instanceof DatabaseError); 207 | assert.ok(error instanceof AppError); 208 | 209 | // Check direct property access across the inheritance hierarchy 210 | assert.equal(error.errorCode, "ER_DUP_ENTRY"); 211 | assert.equal(error.affectedRows, 0); 212 | assert.equal(error.dbName, "customers"); 213 | assert.equal(error.query, "INSERT INTO users (email) VALUES ('existing@example.com')"); 214 | assert.equal(error.appName, "CustomerManagement"); 215 | assert.equal(error.version, "1.0.0"); 216 | 217 | // Check error hierarchy information 218 | const hierarchy = QueryError.getErrorHierarchy(error); 219 | assert.equal(hierarchy.length, 3); 220 | assert.equal(hierarchy[0].name, "QueryError"); 221 | 222 | // Check the inheritanceChain field 223 | assert.deepStrictEqual(hierarchy[0].inheritanceChain, ["AppError", "DatabaseError"]); 224 | 225 | // Check inheritance chain through followParentChain 226 | const chain = QueryError.followParentChain(error); 227 | assert.equal(chain.length, 3); 228 | assert.equal(chain[0].name, "QueryError"); 229 | assert.equal(chain[1].name, "DatabaseError"); 230 | assert.equal(chain[2].name, "AppError"); 231 | }); 232 | }); 233 | 234 | describe("Parent-Child Relationships", () => { 235 | test("should establish basic parent-child relationship between errors", () => { 236 | const NetworkError = createCustomError<{ 237 | hostname: string; 238 | port: number; 239 | }>("NetworkError", ["hostname", "port"]); 240 | 241 | const ServiceError = createCustomError<{ 242 | serviceName: string; 243 | operation: string; 244 | }>("ServiceError", ["serviceName", "operation"]); 245 | 246 | // Create parent error 247 | const netError = new NetworkError({ 248 | message: "Failed to connect to remote server", 249 | cause: { 250 | hostname: "api.example.com", 251 | port: 443, 252 | }, 253 | }); 254 | 255 | // Create child error with parent 256 | const svcError = new ServiceError({ 257 | message: "Authentication service unavailable", 258 | parent: netError, // Pass the error as cause to establish parent relationship 259 | }); 260 | 261 | // Check properties 262 | assert.equal(svcError.name, "ServiceError"); 263 | assert.equal(svcError.message, "Authentication service unavailable"); 264 | 265 | // Check parent relationship 266 | assert.ok(svcError.parent); 267 | assert.equal(svcError.parent, netError); 268 | assert.equal(svcError.parent.name, "NetworkError"); 269 | assert.equal(svcError.parent.message, "Failed to connect to remote server"); 270 | 271 | // Follow the parent chain 272 | const chain = ServiceError.followParentChain(svcError); 273 | assert.equal(chain.length, 2); 274 | assert.equal(chain[0].name, "ServiceError"); 275 | assert.equal(chain[1].name, "NetworkError"); 276 | }); 277 | 278 | test("should create a multi-level error chain", () => { 279 | const SystemError = createCustomError<{ 280 | component: string; 281 | }>("SystemError", ["component"]); 282 | 283 | const FileError = createCustomError<{ 284 | path: string; 285 | operation: string; 286 | }>("FileError", ["path", "operation"]); 287 | 288 | const ConfigError = createCustomError<{ 289 | configKey: string; 290 | expectedType: string; 291 | }>("ConfigError", ["configKey", "expectedType"]); 292 | 293 | // Level 3 (root cause) 294 | const sysError = new SystemError({ 295 | message: "File system unavailable", 296 | cause: { 297 | component: "disk_controller", 298 | }, 299 | }); 300 | 301 | // Level 2 302 | const fileError = new FileError({ 303 | message: "Could not read configuration file", 304 | parent: sysError, // Parent relationship 305 | }); 306 | 307 | // Level 1 (what application code catches) 308 | const confError = new ConfigError({ 309 | message: "Application configuration invalid", 310 | parent: fileError, // Parent relationship 311 | }); 312 | 313 | // Check parent chains 314 | assert.equal(confError.parent, fileError); 315 | assert.equal(fileError.parent, sysError); 316 | 317 | // Access parent error properties 318 | assert.equal(confError.parent?.name, "FileError"); 319 | assert.equal(confError.parent.parent?.name, "SystemError"); 320 | 321 | // Follow complete error chain 322 | const errorChain = ConfigError.followParentChain(confError); 323 | assert.equal(errorChain.length, 3); 324 | assert.equal(errorChain[0].name, "ConfigError"); 325 | assert.equal(errorChain[1].name, "FileError"); 326 | assert.equal(errorChain[2].name, "SystemError"); 327 | 328 | // Get full error hierarchy with contexts 329 | const hierarchy = ConfigError.getErrorHierarchy(confError); 330 | assert.equal(hierarchy.length, 3); 331 | assert.equal(hierarchy[0].name, "ConfigError"); 332 | assert.equal(hierarchy[1].name, "FileError"); 333 | assert.equal(hierarchy[2].name, "SystemError"); 334 | }); 335 | 336 | test("should handle complex parent relationship with context", () => { 337 | const SystemError = createCustomError<{ 338 | component: string; 339 | }>("SystemError", ["component"]); 340 | 341 | const FileError = createCustomError<{ 342 | path: string; 343 | operation: string; 344 | }>("FileError", ["path", "operation"]); 345 | 346 | // Level 3 (root cause) 347 | const sysError = new SystemError({ 348 | message: "File system unavailable", 349 | cause: { 350 | component: "disk_controller", 351 | }, 352 | }); 353 | 354 | // Level 2 - pass the parent error directly as cause 355 | const fileError = new FileError({ 356 | message: "Could not read configuration file", 357 | cause: { 358 | path: "/etc/config.json", 359 | operation: "read", 360 | }, 361 | }); 362 | 363 | // Manually set parent to avoid the spreading issue 364 | Object.defineProperty(fileError, "parent", { 365 | value: sysError, 366 | enumerable: true, 367 | writable: true, 368 | configurable: true, 369 | }); 370 | 371 | // Check direct property access 372 | assert.equal(fileError.path, "/etc/config.json"); 373 | assert.equal(fileError.operation, "read"); 374 | 375 | // Check parent relationship works 376 | assert.ok(fileError.parent); 377 | assert.equal(fileError.parent.name, "SystemError"); 378 | 379 | // Follow parent chain 380 | const chain = FileError.followParentChain(fileError); 381 | assert.equal(chain.length, 2); 382 | assert.equal(chain[0].name, "FileError"); 383 | assert.equal(chain[1].name, "SystemError"); 384 | }); 385 | }); 386 | 387 | describe("Mixed Inheritance and Parent Relationships", () => { 388 | test("should combine class inheritance with parent-child relationships", () => { 389 | // Class inheritance (level 1) 390 | const BaseError = createCustomError<{ 391 | application: string; 392 | }>("BaseError", ["application"]); 393 | 394 | // Class inheritance (level 2) 395 | const DatabaseError = createCustomError< 396 | { 397 | database: string; 398 | }, 399 | typeof BaseError 400 | >("DatabaseError", ["database"], BaseError); 401 | 402 | // Class inheritance (level 3) 403 | const QueryError = createCustomError< 404 | { 405 | query: string; 406 | }, 407 | typeof DatabaseError 408 | >("QueryError", ["query"], DatabaseError); 409 | 410 | // Independent error for parent chain 411 | const NetworkError = createCustomError<{ 412 | host: string; 413 | }>("NetworkError", ["host"]); 414 | 415 | // Create parent error 416 | const netError = new NetworkError({ 417 | message: "Network connection failed", 418 | cause: { 419 | host: "db.example.com", 420 | }, 421 | }); 422 | 423 | // Create child error with inherited context from class hierarchy 424 | // and parent-child relationship to the NetworkError 425 | const queryError = new QueryError({ 426 | message: "Failed to execute query due to connection issue", 427 | cause: { 428 | // QueryError context 429 | query: "SELECT * FROM users", 430 | 431 | // DatabaseError context 432 | database: "main_users", 433 | 434 | // BaseError context 435 | application: "UserService", 436 | }, 437 | }); 438 | 439 | // Test class inheritance 440 | assert.ok(queryError instanceof QueryError); 441 | assert.ok(queryError instanceof DatabaseError); 442 | assert.ok(queryError instanceof BaseError); 443 | 444 | // Test direct property access across inheritance 445 | assert.equal(queryError.query, "SELECT * FROM users"); 446 | assert.equal(queryError.database, "main_users"); 447 | assert.equal(queryError.application, "UserService"); 448 | 449 | // Test inheritance chain via properties 450 | assert.ok(queryError.inheritanceChain); 451 | assert.deepEqual( 452 | queryError.inheritanceChain.map((e) => e.name), 453 | ["BaseError", "DatabaseError"], 454 | ); 455 | }); 456 | }); 457 | 458 | describe("Advanced Usage Patterns", () => { 459 | test("should support dynamic error creation based on domain", () => { 460 | // Factory function to create domain-specific errors 461 | function createDomainErrors(domain: string) { 462 | const BaseDomainError = createCustomError<{ 463 | domain: string; 464 | correlationId: string; 465 | }>(`${domain}Error`, ["domain", "correlationId"]); 466 | 467 | const ValidationError = createCustomError< 468 | { 469 | field: string; 470 | value: unknown; 471 | }, 472 | typeof BaseDomainError 473 | >(`${domain}ValidationError`, ["field", "value"], BaseDomainError); 474 | 475 | return { 476 | BaseDomainError, 477 | ValidationError, 478 | }; 479 | } 480 | 481 | // Create user domain errors 482 | const UserErrors = createDomainErrors("User"); 483 | // Create product domain errors with the same structure 484 | const ProductErrors = createDomainErrors("Product"); 485 | 486 | const userError = new UserErrors.ValidationError({ 487 | message: "Invalid user data provided", 488 | cause: { 489 | field: "email", 490 | value: "not-an-email", 491 | domain: "User", 492 | correlationId: "usr-123-456-789", 493 | }, 494 | }); 495 | 496 | const productError = new ProductErrors.ValidationError({ 497 | message: "Invalid product data provided", 498 | cause: { 499 | field: "price", 500 | value: -10, 501 | domain: "Product", 502 | correlationId: "prod-987-654-321", 503 | }, 504 | }); 505 | 506 | // Verify UserError properties 507 | assert.equal(userError.name, "UserValidationError"); 508 | assert.equal(userError.field, "email"); 509 | assert.equal(userError.value, "not-an-email"); 510 | assert.equal(userError.domain, "User"); 511 | 512 | // Verify ProductError properties 513 | assert.equal(productError.name, "ProductValidationError"); 514 | assert.equal(productError.field, "price"); 515 | assert.equal(productError.value, -10); 516 | assert.equal(productError.domain, "Product"); 517 | 518 | // Check inheritance 519 | assert.ok(userError instanceof UserErrors.BaseDomainError); 520 | assert.ok(productError instanceof ProductErrors.BaseDomainError); 521 | 522 | // Make sure the domains are separate classes 523 | assert.ok(!(userError instanceof ProductErrors.BaseDomainError)); 524 | assert.ok(!(productError instanceof UserErrors.BaseDomainError)); 525 | }); 526 | 527 | test("should support error factory functions", () => { 528 | // Define base error types 529 | const ApiError = createCustomError<{ 530 | endpoint: string; 531 | statusCode: number; 532 | timestamp: string; 533 | }>("ApiError", ["endpoint", "statusCode", "timestamp"]); 534 | 535 | // Factory function for creating user-related API errors 536 | function createUserApiError( 537 | statusCode: number, 538 | endpoint: string, 539 | userId?: string, 540 | action?: string, 541 | ) { 542 | const baseMessage = `User API error (${statusCode})`; 543 | const detailedMessage = userId 544 | ? `${baseMessage}: Failed to ${action || "process"} user ${userId}` 545 | : baseMessage; 546 | 547 | return new ApiError({ 548 | message: detailedMessage, 549 | cause: { 550 | endpoint, 551 | statusCode, 552 | timestamp: new Date().toISOString(), 553 | }, 554 | }); 555 | } 556 | 557 | const error = createUserApiError(404, "/api/users/123", "123", "fetch"); 558 | 559 | assert.equal(error.name, "ApiError"); 560 | assert.equal(error.message, "User API error (404): Failed to fetch user 123"); 561 | assert.equal(error.statusCode, 404); 562 | assert.equal(error.endpoint, "/api/users/123"); 563 | assert.ok(error.timestamp); // Just check it exists as it's dynamic 564 | }); 565 | 566 | test("should handle deeply nested context objects", () => { 567 | // Error with deeply nested context structure 568 | const ConfigurationError = createCustomError<{ 569 | config: { 570 | server: { 571 | host: string; 572 | port: number; 573 | ssl: { 574 | enabled: boolean; 575 | cert?: string; 576 | }; 577 | }; 578 | database: { 579 | connection: { 580 | host: string; 581 | credentials: { 582 | username: string; 583 | encrypted: boolean; 584 | }; 585 | }; 586 | }; 587 | }; 588 | }>("ConfigurationError", ["config"]); 589 | 590 | const error = new ConfigurationError({ 591 | message: "Invalid server configuration", 592 | cause: { 593 | config: { 594 | server: { 595 | host: "localhost", 596 | port: 8080, 597 | ssl: { 598 | enabled: true, 599 | cert: undefined, // Missing certificate 600 | }, 601 | }, 602 | database: { 603 | connection: { 604 | host: "db.example.com", 605 | credentials: { 606 | username: "app_user", 607 | encrypted: false, // Unencrypted credentials 608 | }, 609 | }, 610 | }, 611 | }, 612 | }, 613 | }); 614 | 615 | assert.equal(error.name, "ConfigurationError"); 616 | assert.equal(error.config.server.host, "localhost"); 617 | assert.equal(error.config.server.port, 8080); 618 | assert.equal(error.config.server.ssl.enabled, true); 619 | assert.equal(error.config.server.ssl.cert, undefined); 620 | assert.equal(error.config.database.connection.host, "db.example.com"); 621 | assert.equal(error.config.database.connection.credentials.username, "app_user"); 622 | assert.equal(error.config.database.connection.credentials.encrypted, false); 623 | }); 624 | }); 625 | 626 | describe("Real-World Scenarios", () => { 627 | test("should handle authentication flow errors", () => { 628 | // Define error hierarchy for auth flow 629 | const AuthError = createCustomError<{ 630 | userId?: string; 631 | requestId: string; 632 | }>("AuthError", ["userId", "requestId"]); 633 | 634 | const CredentialsError = createCustomError< 635 | { 636 | reason: "invalid" | "expired" | "locked"; 637 | attemptCount: number; 638 | }, 639 | typeof AuthError 640 | >("CredentialsError", ["reason", "attemptCount"], AuthError); 641 | 642 | const MfaError = createCustomError< 643 | { 644 | mfaType: "sms" | "app" | "email"; 645 | remainingAttempts: number; 646 | }, 647 | typeof AuthError 648 | >("MfaError", ["mfaType", "remainingAttempts"], AuthError); 649 | 650 | // Create a credentials error 651 | const credError = new CredentialsError({ 652 | message: "Invalid credentials provided", 653 | cause: { 654 | requestId: "auth-123", 655 | userId: "user@example.com", 656 | reason: "invalid", 657 | attemptCount: 1, 658 | }, 659 | }); 660 | 661 | // Create an MFA error 662 | const mfaError = new MfaError({ 663 | message: "MFA verification required", 664 | cause: { 665 | requestId: "auth-456", 666 | userId: "user@example.com", 667 | mfaType: "app", 668 | remainingAttempts: 3, 669 | }, 670 | }); 671 | 672 | // Test credentials error 673 | assert.equal(credError.name, "CredentialsError"); 674 | assert.equal(credError.userId, "user@example.com"); 675 | assert.equal(credError.requestId, "auth-123"); 676 | assert.equal(credError.reason, "invalid"); 677 | assert.equal(credError.attemptCount, 1); 678 | assert.ok(credError instanceof AuthError); 679 | 680 | // Test MFA error 681 | assert.equal(mfaError.name, "MfaError"); 682 | assert.equal(mfaError.userId, "user@example.com"); 683 | assert.equal(mfaError.requestId, "auth-456"); 684 | assert.equal(mfaError.mfaType, "app"); 685 | assert.equal(mfaError.remainingAttempts, 3); 686 | assert.ok(mfaError instanceof AuthError); 687 | }); 688 | }); 689 | 690 | describe("Edge Cases and Special Behaviors", () => { 691 | test("should handle string as cause", () => { 692 | const SimpleError = createCustomError<{ code: number }>("SimpleError", ["code"]); 693 | 694 | const error = new SimpleError({ 695 | message: "Test error", 696 | cause: "Original cause message", 697 | }); 698 | 699 | assert.equal(error.name, "SimpleError"); 700 | assert.equal(error.message, "Test error"); 701 | assert.ok(error.parent); 702 | assert.equal(error.parent.message, "Original cause message"); 703 | }); 704 | 705 | test("should handle error serialization with toJSON", () => { 706 | const ApiError = createCustomError<{ 707 | statusCode: number; 708 | endpoint: string; 709 | }>("ApiError", ["statusCode", "endpoint"]); 710 | 711 | const error = new ApiError({ 712 | message: "API error occurred", 713 | cause: { 714 | statusCode: 500, 715 | endpoint: "/api/users", 716 | }, 717 | captureStack: true, 718 | }); 719 | 720 | // Convert to JSON and back 721 | const serialized = JSON.stringify(error); 722 | const deserialized = JSON.parse(serialized); 723 | 724 | assert.equal(deserialized.name, "ApiError"); 725 | assert.equal(deserialized.message, "API error occurred"); 726 | assert.ok(deserialized.stack, "Stack should be included"); 727 | assert.ok(deserialized.cause, "Cause should be included"); 728 | assert.equal(deserialized.cause.statusCode, 500); 729 | assert.equal(deserialized.cause.endpoint, "/api/users"); 730 | }); 731 | 732 | test("should handle null or undefined cause gracefully", () => { 733 | const SimpleError = createCustomError("SimpleError", []); 734 | 735 | // @ts-ignore - Deliberately passing undefined to test error handling 736 | const error1 = new SimpleError({ 737 | message: "Test error", 738 | cause: undefined, 739 | }); 740 | 741 | assert.equal(error1.name, "SimpleError"); 742 | assert.equal(error1.message, "Test error"); 743 | assert.equal(error1.parent, undefined); 744 | 745 | const error2 = new SimpleError({ 746 | message: "Test error", 747 | // @ts-ignore - Deliberately passing null to test error handling 748 | cause: null, 749 | }); 750 | 751 | assert.equal(error2.name, "SimpleError"); 752 | assert.equal(error2.message, "Test error"); 753 | }); 754 | 755 | test("should work with no context keys specified", () => { 756 | const NoContextError = createCustomError("NoContextError", []); 757 | 758 | const error = new NoContextError({ 759 | message: "Error with no context", 760 | }); 761 | 762 | assert.equal(error.name, "NoContextError"); 763 | assert.equal(error.message, "Error with no context"); 764 | 765 | const context = NoContextError.getContext(error); 766 | assert.deepEqual(context, undefined); 767 | }); 768 | 769 | test("should check type with checkInstance correctly", () => { 770 | const TypedError = createCustomError<{ value: number }>("TypedError", ["value"]); 771 | 772 | const error = new TypedError({ 773 | message: "Typed error", 774 | cause: { value: 42 }, 775 | }); 776 | 777 | // TypeScript type guard with checkInstance 778 | if (isError(error, TypedError)) { 779 | assert.equal(error.value, 42); 780 | } else { 781 | assert.fail("checkInstance should have returned true"); 782 | } 783 | 784 | // Check with non-matching error type 785 | const OtherError = createCustomError("OtherError", []); 786 | assert.equal(isError(error, OtherError), false); 787 | 788 | // Check with non-error object 789 | assert.equal(isError({}, TypedError), false); 790 | assert.equal(isError(null, TypedError), false); 791 | assert.equal(isError(undefined, TypedError), false); 792 | }); 793 | 794 | test("should handle default message when not provided", () => { 795 | const SimpleError = createCustomError("SimpleError", []); 796 | 797 | // @ts-ignore - Deliberately not providing message to test defaults 798 | const error = new SimpleError({}); 799 | 800 | assert.equal(error.name, "SimpleError"); 801 | assert.equal(error.message, undefined); 802 | }); 803 | 804 | test("should handle complex inheritance with multiple levels", () => { 805 | // 4-level inheritance hierarchy 806 | const Level1Error = createCustomError<{ level1: string }>("Level1Error", ["level1"]); 807 | 808 | const Level2Error = createCustomError<{ level2: string }, typeof Level1Error>( 809 | "Level2Error", 810 | ["level2"], 811 | Level1Error, 812 | ); 813 | 814 | const Level3Error = createCustomError<{ level3: string }, typeof Level2Error>( 815 | "Level3Error", 816 | ["level3"], 817 | Level2Error, 818 | ); 819 | 820 | const Level4Error = createCustomError<{ level4: string }, typeof Level3Error>( 821 | "Level4Error", 822 | ["level4"], 823 | Level3Error, 824 | ); 825 | 826 | const error = new Level4Error({ 827 | message: "Deep inheritance", 828 | cause: { 829 | level1: "one", 830 | level2: "two", 831 | level3: "three", 832 | level4: "four", 833 | }, 834 | }); 835 | 836 | // Check direct property access 837 | assert.equal(error.level1, "one"); 838 | assert.equal(error.level2, "two"); 839 | assert.equal(error.level3, "three"); 840 | assert.equal(error.level4, "four"); 841 | 842 | // Check inheritance 843 | assert.ok(error instanceof Level1Error); 844 | assert.ok(error instanceof Level2Error); 845 | assert.ok(error instanceof Level3Error); 846 | assert.ok(error instanceof Level4Error); 847 | 848 | // Check inheritance chain 849 | assert.deepEqual( 850 | error?.inheritanceChain?.map((e) => e.name), 851 | ["Level1Error", "Level2Error", "Level3Error"], 852 | ); 853 | }); 854 | 855 | test("should support custom toString formatting", () => { 856 | const DetailedError = createCustomError<{ 857 | code: number; 858 | details: string; 859 | }>("DetailedError", ["code", "details"]); 860 | 861 | const error = new DetailedError({ 862 | message: "A detailed error", 863 | cause: { 864 | code: 500, 865 | details: "Internal server error", 866 | }, 867 | }); 868 | 869 | const errorString = error.toString(); 870 | assert.ok(errorString.includes("DetailedError: A detailed error")); 871 | assert.ok(errorString.includes("code")); 872 | assert.ok(errorString.includes("500")); 873 | assert.ok(errorString.includes("details")); 874 | assert.ok(errorString.includes("Internal server error")); 875 | }); 876 | }); 877 | 878 | describe("Additional Tests", () => { 879 | test("should support JSON serialization/deserialization", () => { 880 | const NetworkError = createCustomError<{ 881 | host: string; 882 | port: number; 883 | }>("NetworkError", ["host", "port"]); 884 | 885 | const error = new NetworkError({ 886 | message: "Connection failed", 887 | cause: { host: "example.com", port: 443 }, 888 | captureStack: true, 889 | }); 890 | 891 | // Serialize to JSON 892 | const serialized = JSON.stringify(error); 893 | const parsed = JSON.parse(serialized); 894 | 895 | // Check basic properties 896 | assert.equal(parsed.name, "NetworkError"); 897 | assert.equal(parsed.message, "Connection failed"); 898 | assert.ok(parsed.stack); // Stack should be present 899 | 900 | // Check cause for context 901 | assert.ok(parsed.cause); 902 | assert.equal(parsed.cause.host, "example.com"); 903 | assert.equal(parsed.cause.port, 443); 904 | }); 905 | 906 | test("should handle inherited context properties correctly", () => { 907 | // Base error with timestamp 908 | const BaseError = createCustomError<{ 909 | timestamp: string; 910 | }>("BaseError", ["timestamp"]); 911 | 912 | // API error with status code 913 | const ApiError = createCustomError<{ statusCode: number }, typeof BaseError>( 914 | "ApiError", 915 | ["statusCode"], 916 | BaseError, 917 | ); 918 | 919 | // Create with all properties 920 | const error = new ApiError({ 921 | message: "API Error", 922 | cause: { 923 | statusCode: 500, 924 | timestamp: "2025-04-04T12:00:00Z", 925 | }, 926 | }); 927 | 928 | // Direct property access 929 | assert.equal(error.statusCode, 500); 930 | assert.equal(error.timestamp, "2025-04-04T12:00:00Z"); 931 | 932 | // Context getters 933 | const fullContext = ApiError.getContext(error); 934 | assert.deepEqual(fullContext, { 935 | statusCode: 500, 936 | timestamp: "2025-04-04T12:00:00Z", 937 | }); 938 | 939 | // Just ApiError context 940 | const apiContext = ApiError.getContext(error, { 941 | includeParentContext: false, 942 | }); 943 | assert.deepEqual(apiContext, { statusCode: 500 }); 944 | }); 945 | 946 | test("should handle error with no context", () => { 947 | const EmptyError = createCustomError("EmptyError", []); 948 | 949 | const error = new EmptyError({ 950 | message: "An error with no context", 951 | }); 952 | 953 | assert.equal(error.name, "EmptyError"); 954 | assert.equal(error.message, "An error with no context"); 955 | 956 | // toString should still work 957 | const errorString = error.toString(); 958 | assert.equal(errorString, "EmptyError: An error with no context"); 959 | 960 | // Context should be undefined 961 | const context = EmptyError.getContext(error); 962 | assert.equal(context, undefined); 963 | }); 964 | }); 965 | 966 | describe("Circular Reference Protection", () => { 967 | test("should detect and prevent circular references", () => { 968 | const ErrorA = createCustomError<{ a: string }>("ErrorA", ["a"]); 969 | const ErrorB = createCustomError<{ b: string }>("ErrorB", ["b"]); 970 | 971 | // Create instance of ErrorA 972 | const errorA = new ErrorA({ 973 | message: "Error A", 974 | cause: { a: "valueA" }, 975 | }); 976 | 977 | // Create instance of ErrorB with ErrorA as parent 978 | const errorB = new ErrorB({ 979 | message: "Error B", 980 | cause: { b: "valueB" }, 981 | }); 982 | 983 | // Set parent property 984 | (errorB as any).parent = errorA; 985 | 986 | // Attempt to create a circular reference 987 | // This should trigger circular reference detection when accessing the parent chain 988 | (errorA as any).parent = errorB; 989 | 990 | // Check if we can safely follow the parent chain without infinite recursion 991 | const chain = ErrorA.followParentChain(errorA); 992 | 993 | // Chain should contain both errors but stop at the circular reference 994 | assert.equal(chain.length, 2); 995 | assert.equal(chain[0].name, "ErrorA"); 996 | assert.equal(chain[1].name, "ErrorB"); 997 | 998 | // Similarly, getErrorHierarchy should handle circular references 999 | const hierarchy = ErrorA.getErrorHierarchy(errorA); 1000 | assert.equal(hierarchy.length, 2); 1001 | assert.equal(hierarchy[0].name, "ErrorA"); 1002 | assert.equal(hierarchy[1].name, "ErrorB"); 1003 | }); 1004 | 1005 | test("should respect maxParentChainLength", () => { 1006 | const BaseError = createCustomError<{ index: number }>("BaseError", ["index"]); 1007 | 1008 | // Create a deeply nested chain 1009 | let previousError: any = null; 1010 | let rootError: any = null; 1011 | 1012 | // Create 10 nested errors 1013 | for (let i = 9; i >= 0; i--) { 1014 | const error = new BaseError({ 1015 | message: `Error ${i}`, 1016 | cause: { index: i }, 1017 | }); 1018 | 1019 | if (previousError) { 1020 | error.parent = previousError; 1021 | } 1022 | 1023 | previousError = error; 1024 | 1025 | if (i === 0) { 1026 | rootError = error; 1027 | } 1028 | } 1029 | 1030 | // Follow chain with default depth 1031 | const fullChain = BaseError.followParentChain(rootError); 1032 | assert.equal(fullChain.length, 10); 1033 | 1034 | // Follow chain with limited depth 1035 | const limitedChain = BaseError.followParentChain(rootError); 1036 | assert.equal(limitedChain.length, 10); 1037 | }); 1038 | 1039 | test("should handle collision strategy", () => { 1040 | const ParentError = createCustomError<{ shared: string }>("ParentError", ["shared"]); 1041 | 1042 | const ChildError = createCustomError<{ shared: string }, typeof ParentError>( 1043 | "ChildError", 1044 | ["shared"], 1045 | ParentError, 1046 | ); 1047 | 1048 | // 'override' strategy (default) 1049 | const error1 = new ChildError({ 1050 | message: "Test", 1051 | cause: { shared: "child-value" }, 1052 | collisionStrategy: "override", 1053 | }); 1054 | 1055 | assert.equal(error1.shared, "child-value"); 1056 | 1057 | // Verify that 'error' strategy throws when there would be a collision 1058 | assert.throws(() => { 1059 | new ChildError({ 1060 | message: "Test", 1061 | cause: { 1062 | shared: "child-value", 1063 | }, 1064 | collisionStrategy: "error", 1065 | }); 1066 | }, /Context property 'shared' conflicts/); 1067 | }); 1068 | }); 1069 | 1070 | describe("Performance Optimizations", () => { 1071 | test("should have faster creation with createFast method", async () => { 1072 | const SimpleError = createCustomError<{ code: number }>("SimpleError", ["code"]); 1073 | 1074 | // This is a simple benchmark, but node:test doesn't have great performance testing 1075 | /** 1076 | * Fast creation should be at least 30% faster than standard creation 1077 | * 1078 | * Average time for 1,000 iterations 1079 | * Standard creation: 7.28ms - 6.50ms 1080 | * Fast creation: 4.21ms - 4.73ms 1081 | * 1082 | * Average time for 10,000 iterations 1083 | * Standard creation: 76.14ms - 74.95ms 1084 | * Fast creation: 44.40ms - 42.82ms 1085 | * 1086 | * Average time for 100,000 iterations 1087 | * Standard creation: 742.12ms - 733.14ms 1088 | * Fast creation: 443.71ms - 454.67ms 1089 | * 1090 | * Average time for 1,000,000 iterations 1091 | * Standard creation: 7590.94ms - 7560.76ms 1092 | * Fast creation: 4469.63ms - 4582.30ms 1093 | */ 1094 | const iterations = 10_000; 1095 | 1096 | // Standard creation 1097 | const startStandard = process.hrtime.bigint(); 1098 | for (let i = 0; i < iterations; i++) { 1099 | new SimpleError({ 1100 | message: "Test error", 1101 | cause: { code: 500 }, 1102 | captureStack: true, 1103 | }); 1104 | } 1105 | const endStandard = process.hrtime.bigint(); 1106 | const standardTime = Number(endStandard - startStandard) / 1_000_000; // ms 1107 | 1108 | // Fast creation 1109 | const startFast = process.hrtime.bigint(); 1110 | for (let i = 0; i < iterations; i++) { 1111 | SimpleError.createFast("Test error", { code: 500 }); 1112 | } 1113 | const endFast = process.hrtime.bigint(); 1114 | const fastTime = Number(endFast - startFast) / 1_000_000; // ms 1115 | 1116 | console.log(`Standard creation: ${standardTime.toFixed(2)}ms`); 1117 | console.log(`Fast creation: ${fastTime.toFixed(2)}ms`); 1118 | 1119 | // Fast creation should be significantly faster 1120 | assert.ok( 1121 | fastTime < standardTime * 0.7, 1122 | `Fast creation (${fastTime.toFixed(2)}ms) should be at least 30% faster than standard (${standardTime.toFixed(2)}ms)`, 1123 | ); 1124 | 1125 | // Verify that the fast creation still produces a valid error 1126 | const fastError = SimpleError.createFast("Fast error", { code: 123 }); 1127 | assert.equal(fastError.message, "Fast error"); 1128 | assert.equal(fastError.code, 123); 1129 | assert.ok(fastError instanceof SimpleError); 1130 | }); 1131 | }); 1132 | -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * # CustomError 3 | * 4 | * A TypeScript library for creating custom error classes with enhanced features such as: 5 | * 6 | * Features: 7 | * - Generate Custom error classes 8 | * - Simplified API for creating custom errors 9 | * - Type-safe error context with TypeScript 10 | * - Inheritance hierarchies with context propagation 11 | * - Parent-child error relationships 12 | * - Custom serialization and formatting 13 | * - Performance optimizations 14 | * - Circular reference detection 15 | * - Property enumeration control 16 | * - Collision strategy for context properties 17 | * - Fast error creation for high-performance scenarios 18 | * 19 | * ## Usage 20 | * 21 | * ```ts 22 | * import { createCustomError, checkInstance } from '@fuzzy-street/errors'; 23 | * 24 | * // Create a custom error class 25 | * const ApiError = createCustomError<{ 26 | * statusCode: number; 27 | * endpoint: string; 28 | * }>( 29 | * "ApiError", 30 | * ["statusCode", "endpoint"] 31 | * ); 32 | * ``` 33 | * 34 | * @see {@link createCustomError} 35 | * @see {@link isError} 36 | * @author aFuzzyBear 37 | * @license MIT 38 | * 39 | */ 40 | 41 | /** 42 | * Type for extracting context from a CustomErrorClass 43 | */ 44 | export type ErrorContext = T extends CustomErrorClass ? Context : Record; 45 | 46 | /** 47 | * Collision strategy for handling context property name collisions 48 | */ 49 | export type CollisionStrategy = "override" | "preserve" | "error"; 50 | 51 | /** 52 | * Options for creating or configuring an error instance 53 | */ 54 | export type CustomErrorOptions | undefined = undefined> = { 55 | message: string; 56 | captureStack?: boolean; 57 | overridePrototype?: ParentError; 58 | enumerableProperties?: boolean | string[]; 59 | collisionStrategy?: CollisionStrategy; 60 | maxParentChainLength?: number; 61 | parent?: Error; 62 | } & ( 63 | | { cause: OwnContext } // Context object 64 | | { cause: string } // Cause message 65 | | { cause?: undefined } // No cause 66 | ); 67 | 68 | /** 69 | * Represents a custom error class with enhanced features 70 | */ 71 | export type CustomErrorClass = { 72 | new( 73 | options: CustomErrorOptions, 74 | ): Error & 75 | T & { 76 | inheritanceChain?: CustomErrorClass[]; 77 | parent?: Error & Partial; // Parent error with potential context 78 | context: T; // Expose context directly on the error 79 | toJSON(): any; // Add toJSON method 80 | }; 81 | 82 | /** 83 | * Retrieves the context data from an error instance 84 | * @param error The error to get context from 85 | * @param options Options for context retrieval 86 | */ 87 | getContext(error: unknown, options?: { includeParentContext?: boolean }): T | undefined; 88 | 89 | /** 90 | * Get full error hierarchy with contexts 91 | * @param error The error to get hierarchy for 92 | */ 93 | getErrorHierarchy(error: unknown): CustomErrorHierarchyItem[]; 94 | 95 | /** 96 | * Follows the chain of parents and returns them as an array 97 | * @param error The error to get parent chain for 98 | */ 99 | followParentChain(error: Error): Error[]; 100 | 101 | /** 102 | * Returns the full inheritance chain of error classes 103 | */ 104 | getInstances(): CustomErrorClass[]; 105 | 106 | /** 107 | * Creates a simplified error with minimal overhead for high-performance scenarios 108 | * @param message Error message 109 | * @param context Optional context object 110 | */ 111 | createFast(message: string, context?: Partial): Error & T; 112 | 113 | prototype: Error; 114 | 115 | /** 116 | * Name of the error class 117 | */ 118 | readonly name: string; 119 | }; 120 | 121 | /** 122 | * Represents a detailed error hierarchy item 123 | */ 124 | export interface CustomErrorHierarchyItem { 125 | name: string; 126 | message: string; 127 | context?: Record; 128 | parent?: string; 129 | inheritanceChain?: string[]; 130 | } 131 | 132 | // WeakMap to store full context 133 | const errorContexts = new WeakMap(); 134 | 135 | // Store context keys per error class 136 | const errorClassKeys = new Map(); 137 | 138 | // Global registry to track all created custom error classes 139 | const customErrorRegistry = new Map>(); 140 | 141 | /** 142 | * Default options for error creation 143 | */ 144 | const DEFAULT_OPTIONS = { 145 | captureStack: true, 146 | enumerableProperties: false, 147 | collisionStrategy: "override" as CollisionStrategy, 148 | maxParentChainLength: 100, 149 | }; 150 | 151 | /** 152 | * Type-safe instance checker for custom errors 153 | * This function provides proper TypeScript type inference when checking error instances 154 | * 155 | * @param error The error to check 156 | * @param instance The custom error class to check against 157 | * @returns Type guard assertion that the error is of type Error & T 158 | * 159 | * @example 160 | * if (checkInstance(error, ApiError)) { 161 | * // TypeScript now knows these properties exist 162 | * console.log(error.statusCode); 163 | * console.log(error.endpoint); 164 | * } 165 | */ 166 | export function isError( 167 | error: unknown, 168 | instance: CustomErrorClass, 169 | ): error is Error & T { 170 | return error instanceof instance; 171 | } 172 | 173 | /** 174 | * Creates a custom error class with enhanced hierarchical error tracking 175 | * 176 | * @param name Name of the error class 177 | * @param contextKeys Array of context property keys 178 | * @param parentError Optional parent error class to inherit from 179 | * @returns A new custom error class with typed context 180 | * 181 | * @example 182 | * const ApiError = createCustomError<{ 183 | * statusCode: number; 184 | * endpoint: string; 185 | * }>("ApiError", ["statusCode", "endpoint"]); 186 | * 187 | * const error = new ApiError({ 188 | * message: "API request failed", 189 | * cause: { statusCode: 404, endpoint: "/api/users" } 190 | * }); 191 | */ 192 | export function createCustomError< 193 | OwnContext extends Record = {}, 194 | ParentError extends CustomErrorClass | undefined = undefined, 195 | >( 196 | name: string, 197 | contextKeys: (keyof OwnContext)[], 198 | parentError?: ParentError, 199 | ): CustomErrorClass< 200 | OwnContext & (ParentError extends CustomErrorClass ? ErrorContext : {}) 201 | > { 202 | // Determine the parent error class 203 | const ParentErrorClass = parentError ?? Error; 204 | 205 | // Store the context keys for this class 206 | errorClassKeys.set(name, contextKeys as string[]); 207 | 208 | class CustomError extends ParentErrorClass { 209 | readonly name: string = name; 210 | inheritanceChain?: CustomErrorClass[]; 211 | parent?: Error; 212 | message!: string; 213 | stack: any; 214 | _contextCached?: boolean; 215 | 216 | constructor(options: CustomErrorOptions) { 217 | // Apply default options 218 | const finalOptions = { 219 | ...DEFAULT_OPTIONS, 220 | ...options, 221 | }; 222 | 223 | // Call parent constructor with just the message 224 | super(finalOptions?.message || "Unknown error"); 225 | 226 | if (finalOptions?.message) { 227 | // Explicitly set the message property 228 | Object.defineProperty(this, "message", { 229 | value: finalOptions.message, 230 | enumerable: false, 231 | writable: true, 232 | configurable: true, 233 | }); 234 | } 235 | 236 | // Now process the options after super() is called 237 | if (finalOptions) { 238 | const { 239 | message, 240 | cause, 241 | captureStack, 242 | parent, 243 | overridePrototype, 244 | enumerableProperties, 245 | collisionStrategy, 246 | maxParentChainLength, 247 | } = finalOptions; 248 | 249 | // Determine which parent to use 250 | const effectiveParent = overridePrototype || parentError; 251 | let mergedContext: Record = {}; 252 | let parentInstance: Error | undefined; 253 | 254 | // Handle parent error if provided 255 | if (parent) { 256 | parentInstance = parent; 257 | 258 | // Extract context from parent if available 259 | const parentContext = errorContexts.get(parent); 260 | if (parentContext) { 261 | mergedContext = { ...parentContext }; 262 | } 263 | } 264 | // Handle various cause types 265 | // if (cause) { 266 | // if (cause instanceof Error) { 267 | // // If cause is an error, use it as the parent 268 | // parentInstance = cause; 269 | 270 | // // Extract context from error if available 271 | // const causeContext = errorContexts.get(cause); 272 | // if (causeContext) { 273 | // mergedContext = { ...causeContext }; 274 | // } 275 | // } else if (typeof cause === "string") { 276 | // // If cause is a string, create a base error 277 | // parentInstance = new Error(cause); 278 | // } else if (typeof cause === "object") { 279 | // // If cause is an object, use it as context 280 | // mergedContext = { ...cause }; 281 | 282 | // // Create parent errors to maintain the error chain 283 | // if (effectiveParent && 284 | // effectiveParent !== (Error as unknown as CustomErrorClass) && 285 | // typeof effectiveParent.getInstances === 'function') { 286 | // try { 287 | // // Create a parent error instance 288 | // const parentKeys = 289 | // errorClassKeys.get(effectiveParent.name) || []; 290 | // const parentContext: Record = {}; 291 | 292 | // // Extract only the keys relevant to the parent 293 | // for (const key of parentKeys) { 294 | // if (key in mergedContext) { 295 | // parentContext[key as string] = mergedContext[key as string]; 296 | // } 297 | // } 298 | 299 | // // Add keys from any ancestor classes 300 | // const ancestorClasses = effectiveParent.getInstances(); 301 | // for (const ancestorClass of ancestorClasses) { 302 | // const ancestorKeys = 303 | // errorClassKeys.get(ancestorClass.name) || []; 304 | // for (const key of ancestorKeys) { 305 | // if (key in mergedContext && !(key in parentContext)) { 306 | // parentContext[key as string] = 307 | // mergedContext[key as string]; 308 | // } 309 | // } 310 | // } 311 | 312 | // parentInstance = new effectiveParent({ 313 | // message: message || `${effectiveParent.name} Error`, 314 | // cause: parentContext, 315 | // captureStack, // Pass captureStack to parent 316 | // collisionStrategy, 317 | // }); 318 | // } catch (e) { 319 | // console.warn( 320 | // `Failed to create ${effectiveParent?.name} instance:`, 321 | // e, 322 | // ); 323 | // } 324 | // } 325 | // } 326 | // } 327 | if (cause) { 328 | if (typeof cause === "string") { 329 | // If cause is a string, create a base error 330 | if (!parentInstance) { 331 | parentInstance = new Error(cause); 332 | } 333 | } else if (typeof cause === "object") { 334 | // If cause is an object, use it as context 335 | mergedContext = { ...cause }; 336 | 337 | // Create parent errors to maintain the error chain 338 | if ( 339 | !parentInstance && 340 | effectiveParent && 341 | effectiveParent !== (Error as unknown as CustomErrorClass) && 342 | typeof effectiveParent.getInstances === "function" 343 | ) { 344 | try { 345 | // Create a parent error instance 346 | const parentKeys = errorClassKeys.get(effectiveParent.name) || []; 347 | const parentContext: Record = {}; 348 | 349 | // Extract only the keys relevant to the parent 350 | for (const key of parentKeys) { 351 | if (key in mergedContext) { 352 | parentContext[key as string] = mergedContext[key as string]; 353 | } 354 | } 355 | 356 | // Add keys from any ancestor classes 357 | const ancestorClasses = effectiveParent.getInstances(); 358 | for (const ancestorClass of ancestorClasses) { 359 | const ancestorKeys = errorClassKeys.get(ancestorClass.name) || []; 360 | for (const key of ancestorKeys) { 361 | if (key in mergedContext && !(key in parentContext)) { 362 | parentContext[key as string] = mergedContext[key as string]; 363 | } 364 | } 365 | } 366 | 367 | parentInstance = new effectiveParent({ 368 | message: message || `${effectiveParent.name} Error`, 369 | cause: parentContext, 370 | captureStack, // Pass captureStack to parent 371 | collisionStrategy, 372 | }); 373 | } catch (e) { 374 | console.warn(`Failed to create ${effectiveParent?.name} instance:`, e); 375 | } 376 | } 377 | } 378 | } 379 | 380 | // Set name properties 381 | Object.defineProperty(this, "name", { 382 | value: name, 383 | enumerable: false, 384 | configurable: true, 385 | }); 386 | 387 | // Assign parent 388 | if (parentInstance) { 389 | // Check for circular references 390 | if (this === parentInstance || this.isInParentChain(parentInstance)) { 391 | console.warn(`Circular reference detected when setting parent of ${name}`); 392 | } else { 393 | Object.defineProperty(this, "parent", { 394 | value: parentInstance, 395 | enumerable: true, 396 | writable: true, 397 | configurable: true, 398 | }); 399 | } 400 | } 401 | 402 | // Build inheritance chain based on effective parent 403 | this.inheritanceChain = 404 | effectiveParent && 405 | effectiveParent !== (Error as unknown as CustomErrorClass) && 406 | typeof effectiveParent.getInstances === "function" 407 | ? [...(effectiveParent.getInstances?.() || []), effectiveParent] 408 | : []; 409 | 410 | // Handle context collisions 411 | if (collisionStrategy === "error") { 412 | this.checkContextCollisions(mergedContext); 413 | } 414 | 415 | // Store the full context 416 | if (Object.keys(mergedContext).length > 0) { 417 | errorContexts.set(this, { ...mergedContext }); 418 | 419 | // Assign all context properties to the error instance 420 | Object.assign(this, mergedContext); 421 | } 422 | 423 | // Handle stack trace 424 | if (captureStack && typeof Error?.captureStackTrace === "function") { 425 | Error.captureStackTrace(this, CustomError); 426 | } else if (captureStack) { 427 | // Fallback for environments without captureStackTrace 428 | this.stack = new Error().stack; 429 | } 430 | 431 | // Handle enumerable properties 432 | if (enumerableProperties) { 433 | this.makePropertiesEnumerable(enumerableProperties); 434 | } 435 | 436 | // Store the maxParentChainLength in the error instance for later use 437 | if (maxParentChainLength) { 438 | Object.defineProperty(this, "maxParentChainLength", { 439 | value: maxParentChainLength, 440 | enumerable: false, 441 | configurable: true, 442 | }); 443 | } 444 | } 445 | } 446 | 447 | /** 448 | * Checks if an error is in the parent chain to detect circular references 449 | */ 450 | private isInParentChain(potentialParent: Error): boolean { 451 | let current: any = this.parent; 452 | const seen = new WeakSet(); 453 | 454 | while (current) { 455 | if (seen.has(current)) { 456 | return true; // Circular reference already exists 457 | } 458 | 459 | if (current === potentialParent) { 460 | return true; 461 | } 462 | 463 | seen.add(current); 464 | current = current.parent; 465 | } 466 | 467 | return false; 468 | } 469 | 470 | /** 471 | * Checks for context property name collisions and throws an error if found 472 | */ 473 | private checkContextCollisions(context: Record): void { 474 | // Get parent context keys 475 | const parentKeys: string[] = []; 476 | 477 | // First, get parent context keys from inheritance chain 478 | if (this.inheritanceChain) { 479 | for (const parentClass of this.inheritanceChain) { 480 | const classKeys = errorClassKeys.get(parentClass.name) || []; 481 | parentKeys.push(...classKeys); 482 | } 483 | } 484 | 485 | // Check for collisions with parent context keys 486 | for (const key in context) { 487 | if (parentKeys.includes(key)) { 488 | throw new Error( 489 | `Context property '${key}' conflicts with an existing property in parent context`, 490 | ); 491 | } 492 | 493 | // Also check for collisions with standard Error properties 494 | if (["name", "message", "stack", "toString", "constructor"].includes(key)) { 495 | throw new Error(`Context property '${key}' conflicts with a standard Error property`); 496 | } 497 | } 498 | } 499 | 500 | /** 501 | * Makes selected properties enumerable 502 | */ 503 | private makePropertiesEnumerable(enumerableProps: boolean | string[]): void { 504 | const propsToMakeEnumerable = 505 | typeof enumerableProps === "boolean" ? ["name", "message", "stack"] : enumerableProps; 506 | 507 | for (const prop of propsToMakeEnumerable) { 508 | if (Object.prototype.hasOwnProperty.call(this, prop)) { 509 | Object.defineProperty(this, prop, { 510 | enumerable: true, 511 | configurable: true, 512 | }); 513 | } 514 | } 515 | } 516 | 517 | // Removed compatibility mode method as it's not needed 518 | 519 | /** 520 | * Custom toString method to include context and inheritance 521 | */ 522 | toString(): string { 523 | const baseString = `${this.name}: ${this.message}`; 524 | const context = errorContexts.get(this); 525 | const inheritanceInfo = 526 | this.inheritanceChain && this.inheritanceChain.length > 0 527 | ? `\nInheritance Chain: ${this.inheritanceChain.map((e) => e.name).join(" > ")}` 528 | : ""; 529 | const parentInfo = this.parent ? `\nParent: ${this.parent.name}: ${this.parent.message}` : ""; 530 | 531 | return context 532 | ? `${baseString}\nCause: ${JSON.stringify(context, null, 2)}${inheritanceInfo}${parentInfo}` 533 | : baseString; 534 | } 535 | 536 | /** 537 | * Custom toJSON method for proper serialization with JSON.stringify 538 | */ 539 | toJSON(): any { 540 | const context = errorContexts.get(this); 541 | 542 | // Create a base object with standard error properties 543 | const result: Record = { 544 | name: this.name, 545 | message: this.message, 546 | }; 547 | 548 | // Add stack if available 549 | if (this.stack) { 550 | result.stack = this.stack; 551 | } 552 | 553 | // Add context if available 554 | if (context) { 555 | result.cause = { ...context }; 556 | } 557 | 558 | // Add parent info if available 559 | if (this.parent) { 560 | result.parent = { 561 | name: this.parent.name, 562 | message: this.parent.message, 563 | }; 564 | 565 | // Add parent context if available 566 | const parentContext = this.parent instanceof Error && errorContexts.get(this.parent); 567 | if (parentContext) { 568 | result.parent.cause = { ...parentContext }; 569 | } 570 | } 571 | 572 | // Add inheritance chain if available 573 | if (this.inheritanceChain && this.inheritanceChain.length > 0) { 574 | result.inheritanceChain = this.inheritanceChain.map((e) => e.name); 575 | } 576 | 577 | return result; 578 | } 579 | } 580 | 581 | // Ensure name is correctly set on the constructor 582 | Object.defineProperty(CustomError, "name", { value: name }); 583 | 584 | // Add static methods 585 | Object.defineProperties(CustomError, { 586 | /** 587 | * Retrieves the context data from an error instance 588 | */ 589 | getContext: { 590 | value: ( 591 | error: unknown, 592 | options?: { includeParentContext?: boolean }, 593 | ): 594 | | (OwnContext & 595 | (ParentError extends CustomErrorClass ? ErrorContext : {})) 596 | | undefined => { 597 | if (!(error instanceof Error)) return undefined; 598 | 599 | const fullContext = errorContexts.get(error); 600 | if (!fullContext) return undefined; 601 | 602 | if (options?.includeParentContext !== false) { 603 | // Return the full context 604 | return fullContext; 605 | } 606 | 607 | // If we only want this class's context, filter for the specified keys 608 | const result: Record = {}; 609 | const keys = errorClassKeys.get(name); 610 | if (keys) { 611 | for (const key of keys) { 612 | if (key in fullContext) { 613 | result[key] = fullContext[key]; 614 | } 615 | } 616 | } 617 | 618 | return Object.keys(result).length > 0 ? (result as any) : undefined; 619 | }, 620 | enumerable: false, 621 | configurable: true, 622 | }, 623 | 624 | /** 625 | * Get full error hierarchy with contexts 626 | */ 627 | getErrorHierarchy: { 628 | value: (error: unknown): CustomErrorHierarchyItem[] => { 629 | if (!(error instanceof Error)) return []; 630 | 631 | const hierarchy: CustomErrorHierarchyItem[] = []; 632 | const seen = new WeakSet(); 633 | let currentError: 634 | | (Error & { 635 | inheritanceChain?: CustomErrorClass[]; 636 | parent?: Error; 637 | }) 638 | | undefined = error; 639 | 640 | while (currentError) { 641 | // Check for circular references 642 | if (seen.has(currentError)) { 643 | console.warn("Circular reference detected in error hierarchy"); 644 | break; 645 | } 646 | seen.add(currentError); 647 | 648 | const hierarchyItem: CustomErrorHierarchyItem = { 649 | name: currentError.name, 650 | message: currentError.message, 651 | context: errorContexts.get(currentError), 652 | inheritanceChain: currentError.inheritanceChain 653 | ? currentError.inheritanceChain.map((e) => e.name) 654 | : undefined, 655 | }; 656 | 657 | // Add parent if it exists 658 | if (currentError.parent) { 659 | hierarchyItem.parent = `${currentError.parent.name}: ${currentError.parent.message}`; 660 | } 661 | 662 | hierarchy.push(hierarchyItem); 663 | 664 | // Move to the next error in the chain 665 | currentError = currentError.parent as 666 | | (Error & { 667 | inheritanceChain?: CustomErrorClass[]; 668 | parent?: Error; 669 | }) 670 | | undefined; 671 | } 672 | 673 | return hierarchy; 674 | }, 675 | enumerable: false, 676 | configurable: true, 677 | }, 678 | 679 | /** 680 | * Follows the chain of parents and returns them as an array 681 | */ 682 | followParentChain: { 683 | value: (error: Error & { parent?: Error }, maxDepth = 100): Error[] => { 684 | const chain = [error]; 685 | let current = error.parent; 686 | const seen = new WeakSet([error]); 687 | let depth = 0; 688 | 689 | while (current && depth < maxDepth) { 690 | if (seen.has(current)) { 691 | console.warn("Circular reference detected in error chain"); 692 | break; 693 | } 694 | seen.add(current); 695 | chain.push(current); 696 | current = (current as any).parent; 697 | depth++; 698 | } 699 | 700 | if (depth >= maxDepth && current) { 701 | console.warn(`Maximum parent chain depth (${maxDepth}) reached`); 702 | } 703 | 704 | return chain; 705 | }, 706 | enumerable: false, 707 | configurable: true, 708 | }, 709 | 710 | /** 711 | * Returns the inheritance chain of error classes 712 | */ 713 | getInstances: { 714 | value: (): CustomErrorClass[] => { 715 | if (!parentError || parentError === (Error as unknown as ParentError)) { 716 | // If no parent, return empty array 717 | return []; 718 | } 719 | 720 | // If parent exists, get its instances and add parent 721 | const parentChain = 722 | typeof parentError.getInstances === "function" ? parentError.getInstances?.() || [] : []; 723 | return [...parentChain, parentError]; 724 | }, 725 | enumerable: false, 726 | configurable: true, 727 | }, 728 | 729 | /** 730 | * Creates a simplified error with minimal overhead for high-performance scenarios 731 | */ 732 | createFast: { 733 | value: (message: string, context?: Partial): Error & OwnContext => { 734 | const error = new CustomError({ 735 | message, 736 | //@ts-expect-error - context is not strictly typed 737 | cause: context || {}, 738 | captureStack: false, 739 | enumerableProperties: false, 740 | collisionStrategy: "override", 741 | }); 742 | if (context) { 743 | Object.assign(error, context); 744 | } 745 | 746 | // @ts-expect-error - We are creating a new instance of CustomError 747 | return error; 748 | }, 749 | enumerable: false, 750 | configurable: true, 751 | }, 752 | }); 753 | 754 | // Store the custom error class in registry with proper name 755 | customErrorRegistry.set(name, CustomError as any); 756 | 757 | return CustomError as unknown as CustomErrorClass< 758 | OwnContext & (ParentError extends CustomErrorClass ? ErrorContext : {}) 759 | >; 760 | } 761 | 762 | /** 763 | * Get a registered error class by name 764 | * 765 | * @param name The name of the error class to retrieve 766 | * @returns The custom error class or undefined if not found 767 | * 768 | * @example 769 | * ```ts 770 | * const ApiError = getErrorClass("ApiError"); 771 | * if (ApiError) { 772 | * const error = new ApiError({ 773 | * message: "API request failed", 774 | * cause: { statusCode: 404, endpoint: "/api/users" } 775 | * }); 776 | * console.log(error.toString()); 777 | * } 778 | * ``` 779 | */ 780 | export function getErrorClass(name: string): CustomErrorClass | undefined { 781 | return customErrorRegistry.get(name); 782 | } 783 | 784 | /** 785 | * List all registered error class names 786 | * 787 | * @returns An array of registered error class names 788 | * 789 | * @example 790 | * ```ts 791 | * const errorClasses = listErrorClasses(); 792 | * console.log("Registered error classes:", errorClasses); 793 | * ``` 794 | * 795 | */ 796 | export function listErrorClasses(): string[] { 797 | return Array.from(customErrorRegistry.keys()); 798 | } 799 | 800 | /** 801 | * Clear all registered error classes (useful for testing) 802 | * 803 | * @example 804 | * ```ts 805 | * clearErrorRegistry(); 806 | * const errorClasses = listErrorClasses(); 807 | * console.log("Registered error classes after clearing:", errorClasses); 808 | * ``` 809 | */ 810 | export function clearErrorRegistry(): void { 811 | customErrorRegistry.clear(); 812 | errorClassKeys.clear(); 813 | } 814 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2022", 4 | "module": "ESNext", 5 | "moduleResolution": "Bundler", 6 | "esModuleInterop": true, 7 | "strict": true, 8 | "declaration": true, 9 | "declarationMap": true, 10 | "sourceMap": true, 11 | "outDir": "./dist", 12 | "rootDir": "./src" 13 | }, 14 | "include": ["src/**/*"] 15 | } -------------------------------------------------------------------------------- /tsup.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'tsup'; 2 | 3 | export default defineConfig({ 4 | entry: ['src/main.ts'], 5 | format: ['cjs', 'esm'], 6 | dts: true, 7 | sourcemap: true, 8 | clean: true, 9 | minify: true, 10 | splitting:true, 11 | treeshake: true, 12 | }); --------------------------------------------------------------------------------