├── .eslintrc.json ├── .gitignore ├── .markdownlint.json ├── .prettierignore ├── .textlintrc.js ├── .travis.yml ├── LICENSE ├── README.md ├── index.js ├── package-lock.json ├── package.json ├── prettier.config.js ├── renovate.json └── src ├── index.js └── test.js /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "browser": true, 4 | "commonjs": true, 5 | "es6": true, 6 | "node": true 7 | }, 8 | "extends": ["eslint:recommended", "plugin:prettier/recommended"], 9 | "parserOptions": { 10 | "sourceType": "module", 11 | "ecmaVersion": 2019 12 | }, 13 | "rules": { 14 | "linebreak-style": ["error", "unix"] 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | 8 | # Runtime data 9 | pids 10 | *.pid 11 | *.seed 12 | *.pid.lock 13 | 14 | # Directory for instrumented libs generated by jscoverage/JSCover 15 | lib-cov 16 | 17 | # Coverage directory used by tools like istanbul 18 | coverage 19 | 20 | # nyc test coverage 21 | .nyc_output 22 | 23 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 24 | .grunt 25 | 26 | # Bower dependency directory (https://bower.io/) 27 | bower_components 28 | 29 | # node-waf configuration 30 | .lock-wscript 31 | 32 | # Compiled binary addons (https://nodejs.org/api/addons.html) 33 | build/Release 34 | 35 | # Dependency directories 36 | node_modules/ 37 | jspm_packages/ 38 | 39 | # TypeScript v1 declaration files 40 | typings/ 41 | 42 | # Optional npm cache directory 43 | .npm 44 | 45 | # Optional eslint cache 46 | .eslintcache 47 | 48 | # Optional REPL history 49 | .node_repl_history 50 | 51 | # Output of 'npm pack' 52 | *.tgz 53 | 54 | # Yarn Integrity file 55 | .yarn-integrity 56 | 57 | # dotenv environment variables file 58 | .env 59 | 60 | # next.js build output 61 | .next 62 | -------------------------------------------------------------------------------- /.markdownlint.json: -------------------------------------------------------------------------------- 1 | { 2 | "default": true, 3 | "line-length": false 4 | } 5 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | package-lock.json 2 | node_modules/ 3 | -------------------------------------------------------------------------------- /.textlintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | filters: {}, 3 | rules: { 4 | '@textlint-rule/no-invalid-control-character': true, 5 | 'common-misspellings': { 6 | ignore: [] 7 | }, 8 | terminology: { 9 | defaultTerms: true 10 | } 11 | } 12 | }; 13 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | sudo: false 3 | node_js: 4 | - '10' 5 | - '12' 6 | install: 7 | - npm install 8 | script: 9 | - npm run test-coverage-ci 10 | after_success: 11 | - npm run show-coverage-ci 12 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Eric Elliott 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 | # MatchErrors 2 | 3 | # Status: Deprecated in favor of [error-causes](https://github.com/paralleldrive/error-causes) which uses the ES2022 error `.cause` property for matching. 4 | 5 | Quickly match errors to the appropriate error handler. No more if/switch/else/case messes. 6 | 7 | ## Why MatchErrors? 8 | 9 | Frequently when dealing with APIs, a call can fail in several different ways, which may require different error handlers for each one. In some languages, the problem is neatly handled using pattern matching. There is a pattern matching proposal in the works for JavaScript, but even with pattern matching, you may be tempted to handle your error dispatch in a way that is not appropriate for JavaScript. Take the following example: 10 | 11 | ```js 12 | class UserNotFoundError extends Error {} 13 | class PermissionDeniedError extends Error {} 14 | 15 | const fetchUser = id => { 16 | if (id === '345') throw new PermissionDeniedError('PermissionDeniedError: You do not have permission to access this resource.'); 17 | throw new UserNotFoundError('UserNotFound: The requested user id, ${ id } was not found.'); 18 | }; 19 | 20 | fetchUser('123').catch(e => { 21 | // This sometimes works, but can easily fail. 22 | // In general, you should avoid checking the class of 23 | // an object in any language, but JavaScript in 24 | // particular because prototypes are dynamic and 25 | // cross-realm access is common, which can both make 26 | // `instanceof` lie. 27 | if (e instanceof UserNotFoundError) { 28 | console.log('No user with the requested id was found.'); 29 | } else if (e instanceof PermissionDeniedError) { 30 | console.log('You do not have permission to access this resource.'); 31 | } else { 32 | throw e; 33 | } 34 | }); 35 | 36 | fetchUser('345').catch(e => { 37 | // A better alternative is to tag the object with 38 | // a property you can inspect. In this case, we'll 39 | // use the error.message field to identify the error. 40 | if (e.message.includes('UserNotFound')) { 41 | console.log('No user with the requested id was found.'); 42 | } else if (e.message.includes('PermissionDeniedError')) { 43 | console.log('You do not have permission to access this resource.'); 44 | } else { 45 | throw e; 46 | } 47 | }); 48 | ``` 49 | 50 | ## What is MatchErrors? 51 | 52 | MatchErrors offers a handy solution for error branching inspired by pattern matching. With MatchErrors, our error matching can look like this: 53 | 54 | ```js 55 | fetchUser('123').catch( 56 | handleErrors({ 57 | UserNotFound: e => console.log('No user with the requested id was found.'), 58 | PermissionDenied: e => console.log('You do not have permission to access this resource.') 59 | }, e); 60 | ); 61 | ``` 62 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | const matchErrors = require('./src/index.js'); 2 | 3 | module.exports = matchErrors; 4 | module.exports.default = matchErrors; 5 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "match-errors", 3 | "version": "1.0.0", 4 | "description": "Quickly match errors to the appropriate error handler. No more if/switch/else/case messes.", 5 | "main": "index.js", 6 | "scripts": { 7 | "lint": "eslint --fix src && echo 'Lint complete.'", 8 | "typecheck": "npx -p typescript tsc --rootDir . src/test.js --allowJs --checkJs --noEmit --lib es6 --jsx react && echo 'TypeScript check complete.'", 9 | "ts": "npm run -s typecheck", 10 | "unit": "node src/test.js", 11 | "test": "npm run -s unit && npm run -s lint", 12 | "watch": "watch 'clear && npm run -s unit && npm run -s lint' src", 13 | "debug": "node --inspect-brk src/test.js", 14 | "test-coverage": "nyc npm test", 15 | "test-coverage-ci": "nyc --reporter=text-lcov npm test", 16 | "show-coverage-ci": "nyc report --reporter=text-lcov | coveralls", 17 | "show-coverage-text": "nyc report --reporter=text || echo \"Run 'npm run test-coverage' first.\"", 18 | "show-coverage-html": "open coverage/index.html || echo \"Run 'npm run test-coverage' first.\"", 19 | "watch": "watch 'clear && npm run -s unit | tap-nirvana && npm run -s lint' src", 20 | "precommit": "npm run -s test" 21 | }, 22 | "repository": { 23 | "type": "git", 24 | "url": "git+https://github.com/ericelliott/match-errors.git" 25 | }, 26 | "keywords": [], 27 | "author": "", 28 | "license": "MIT", 29 | "bugs": { 30 | "url": "https://github.com/ericelliott/match-errors/issues" 31 | }, 32 | "homepage": "https://github.com/ericelliott/match-errors#readme", 33 | "devDependencies": { 34 | "coveralls": "3.1.0", 35 | "eslint": "7.29.0", 36 | "eslint-config-prettier": "7.2.0", 37 | "eslint-plugin-prettier": "3.4.0", 38 | "markdownlint-cli": "0.27.1", 39 | "nyc": "14.1.1", 40 | "prettier": "1.19.1", 41 | "riteway": "6.2.1", 42 | "tap-nirvana": "1.1.0", 43 | "textlint": "11.9.1", 44 | "textlint-rule-common-misspellings": "1.0.1", 45 | "textlint-rule-terminology": "2.1.5", 46 | "watch": "1.0.2" 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /prettier.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | singleQuote: true 3 | }; 4 | -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "config:base" 4 | ], 5 | "automerge": true, 6 | "automergeType": "branch", 7 | "major": { 8 | "automerge": false 9 | }, 10 | "schedule": "before 4am", 11 | "timezone": "America/Los_Angeles" 12 | } 13 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | const curry = (f, arr = []) => (...args) => 2 | (a => (a.length === f.length ? f(...a) : curry(f, a)))([...arr, ...args]); 3 | 4 | const handleErrors = curry( 5 | ( 6 | { errorMatches = (key, error) => error.message.includes(key), ...handlers }, 7 | error 8 | ) => { 9 | const entries = Object.entries(handlers); 10 | if (entries.length < 1) throw error; 11 | 12 | const isHandled = Object.entries(handlers).reduce( 13 | (isHandled, [key, handler]) => { 14 | if (isHandled) return isHandled; 15 | if (errorMatches(key, error)) { 16 | handler(error); 17 | return true; 18 | } 19 | }, 20 | false 21 | ); 22 | 23 | if (!isHandled) throw error; 24 | } 25 | ); 26 | 27 | module.exports = handleErrors; 28 | -------------------------------------------------------------------------------- /src/test.js: -------------------------------------------------------------------------------- 1 | const { describe, Try } = require('riteway'); 2 | 3 | const handleErrors = require('./index.js'); 4 | 5 | describe('Tests', async assert => { 6 | assert({ 7 | given: 'no arguments', 8 | should: 'run', 9 | actual: true, 10 | expected: true 11 | }); 12 | }); 13 | 14 | // handleErrors = (handlers, error) => Void 15 | // auto-curried version: 16 | // handleErrors = handlers => e => Void 17 | 18 | describe('Match Errors', async assert => { 19 | // const messages = []; 20 | // const log = message => messages.push(message); 21 | const error = new Error('Something is wrong.'); 22 | const result = Try(handleErrors, {}, error); 23 | 24 | assert({ 25 | given: 'no error handlers', 26 | should: 'rethrow the error', 27 | actual: result.message, 28 | expected: error.message 29 | }); 30 | 31 | { 32 | const messages = []; 33 | const log = message => messages.push(message); 34 | const error = new Error('SomethingWrong: Something is wrong.'); 35 | 36 | Try( 37 | handleErrors, 38 | { 39 | SomethingWrong: log 40 | }, 41 | error 42 | ); 43 | 44 | assert({ 45 | given: 'a matching error handler', 46 | should: 'run the matching error handler', 47 | actual: messages.length, 48 | expected: 1 49 | }); 50 | } 51 | 52 | { 53 | const messages = []; 54 | const log = message => messages.push(message); 55 | const error = new Error('SomethingWrong: Something is wrong.'); 56 | const WrongHandler = () => { 57 | throw new Error('Wrong handler called!'); 58 | }; 59 | 60 | Try( 61 | handleErrors, 62 | { 63 | WrongHandler, 64 | SomethingWrong: log 65 | }, 66 | error 67 | ); 68 | 69 | assert({ 70 | given: 'multiple handlers', 71 | should: 'call the matching handler and not call other handlers', 72 | actual: messages.length, 73 | expected: 1 74 | }); 75 | } 76 | 77 | { 78 | const messages = []; 79 | const log = message => messages.push(message); 80 | const error = new Error('SomethingWrong: Something is wrong.'); 81 | const WrongHandler = () => { 82 | throw new Error('Wrong handler called!'); 83 | }; 84 | 85 | const resultError = Try( 86 | handleErrors, 87 | { 88 | SomethingWrong: log, 89 | WrongHandler 90 | }, 91 | error 92 | ); 93 | 94 | assert({ 95 | given: 'multiple handlers with matching handler first', 96 | should: 'call the matching handler and not throw', 97 | actual: resultError, 98 | expected: undefined 99 | }); 100 | } 101 | 102 | { 103 | const WrongHandler = () => { 104 | throw new Error('Wrong handler called!'); 105 | }; 106 | const WrongHandler2 = () => { 107 | throw new Error('Wrong handler 2 called!'); 108 | }; 109 | const error = new Error('SomethingWrong: Something is wrong.'); 110 | 111 | const actualError = Try( 112 | handleErrors, 113 | { 114 | WrongHandler, 115 | WrongHandler2 116 | }, 117 | error 118 | ); 119 | 120 | assert({ 121 | given: 'multiple handlers with no matches', 122 | should: 'rethrow the error', 123 | actual: actualError && actualError.message, 124 | expected: error.message 125 | }); 126 | } 127 | 128 | { 129 | const error = new Error('SomethingWrong: Something is wrong.'); 130 | 131 | const result = Try( 132 | handleErrors, 133 | { 134 | SomethingWrong: () => {} 135 | }, 136 | error 137 | ); 138 | 139 | assert({ 140 | given: 'a matching error handler', 141 | should: 'not throw an error', 142 | actual: result, 143 | expected: undefined 144 | }); 145 | } 146 | 147 | { 148 | const messages = []; 149 | const log = message => messages.push(message); 150 | const error = new Error('SomethingWrong: Something is wrong.'); 151 | 152 | const checkErrors = handleErrors({ 153 | SomethingWrong: log 154 | }); 155 | 156 | checkErrors(error); 157 | 158 | assert({ 159 | given: 'handlers, partially applied', 160 | should: 'run the matching error function', 161 | actual: messages.length, 162 | expected: 1 163 | }); 164 | } 165 | }); 166 | --------------------------------------------------------------------------------