├── .gitignore ├── .istanbul.yml ├── MIGRATION.md ├── README.md ├── benchmarks └── index.js ├── index.js ├── package.json ├── test ├── index.js ├── multi.js ├── typed.js └── wrapped.js ├── tsconfig.json └── types └── pre-bundled__tape ├── index.d.ts ├── tsconfig.json └── tslint.json /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .monitor 3 | .*.swp 4 | .nodemonignore 5 | releases 6 | *.log 7 | *.err 8 | fleet.json 9 | public/browserify 10 | bin/*.json 11 | .bin 12 | build 13 | compile 14 | .lock-wscript 15 | node_modules 16 | coverage 17 | .vscode/ 18 | package-lock.json 19 | 20 | -------------------------------------------------------------------------------- /.istanbul.yml: -------------------------------------------------------------------------------- 1 | instrumentation: 2 | default-excludes: false 3 | include-all-sources: true 4 | excludes: 5 | - '**/test/**' 6 | - '**/coverage/**' 7 | - '**/example/**' 8 | - '**/test.js' 9 | - '**/node_modules/istanbul/**' 10 | - '**/node_modules/tape/**' 11 | - '**/node_modules/uber-standard/**' 12 | -------------------------------------------------------------------------------- /MIGRATION.md: -------------------------------------------------------------------------------- 1 | ## Migration 2 | 3 | ## Version 10 4 | 5 | The 10th version is a complete rewrite. Check out the `v7.x` 6 | branch for the older implementation. 7 | 8 | ## Version 7 9 | 10 | The `message` parameter to `TypedError` is now 11 | required. Previously `message` was optional 12 | for `TypedError`. 13 | 14 | ## Version 6 15 | 16 | The `WrappedError` class now exposes the error that 17 | is being wrapped as a `cause` field instead of an 18 | `original` field. 19 | 20 | The following properties have been reserver on the 21 | wrapped error class: `cause`, `fullType`, `causeMessage` 22 | 23 | ## Version 5 24 | 25 | There were no breaking changes... 26 | 27 | ## Version 4 28 | 29 | The `TypedError` function now has mandatory arguments. 30 | The `type` and `message` arguments for `TypedError` 31 | are required. 32 | 33 | ## Version 3 34 | 35 | The `TypedError` class now uses `string-template` for 36 | message formatting. 37 | 38 | Previously: 39 | 40 | ```js 41 | var FooError = TypedError({ 42 | type: 'foo.x' 43 | message: 'Got an error %s' 44 | }); 45 | 46 | FooError('Oops'); 47 | ``` 48 | 49 | Currently: 50 | 51 | ```js 52 | var FooError = TypedError({ 53 | type: 'foo.x', 54 | message: 'Got an error {ctx}', 55 | ctx: null 56 | }); 57 | 58 | FooError({ ctx: 'Oops' }); 59 | ``` 60 | 61 | ## Version 2 62 | 63 | Original version 64 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # error 2 | 3 | Wrap errors with more context. 4 | 5 | ## Inspiration 6 | 7 | This module is inspired by the go error libraries that have simple 8 | functions for creating & wrapping errors. 9 | 10 | This is based on libraries like [eris][eris] & [pkg/errors][pkg-errors] 11 | 12 | ## Older version of `error` 13 | 14 | If you are looking for the older v7 version of error you should 15 | check [v7.x][7.x] branch 16 | 17 | ## Using `error` with `async` / `await` 18 | 19 | Check out [`resultify`](https://www.npmjs.com/package/resultify) ! 20 | 21 | The rest of the examples use plain vanilla callbacks. 22 | 23 | ## Motivation 24 | 25 | Wrapping errors when bubbling up instead of just doing 26 | `if (err) return cb(err)` allows you to pass more context 27 | up the stack. 28 | 29 | Common example include passing along parameters from the DB 30 | read related to the failure or passing along any context 31 | from the user in a HTTP request when doing a failure. 32 | 33 | This can give you nice to read messages that include more 34 | information about the failure as it bubbles up. 35 | 36 | There is more information about how to handle errors in this 37 | article [Don't just check errors, handle them gracefully][dave] 38 | 39 | If you want a deep dive into the difference between 40 | [Programming and Operational errors](https://www.joyent.com/node-js/production/design/errors) 41 | please check out [this guide](https://www.joyent.com/node-js/production/design/errors) 42 | 43 | examples: 44 | 45 | ```js 46 | const { wrapf } = require('error') 47 | 48 | function authenticatRequest(req) { 49 | authenticate(req.user, (err) => { 50 | if (err) { 51 | return cb(wrapf('authenticate failed', err)) 52 | } 53 | cb(null) 54 | }) 55 | } 56 | ``` 57 | 58 | or 59 | 60 | ```js 61 | const { wrapf } = require('error') 62 | 63 | function readFile(path, cb) { 64 | fs.open(path, 'r', (err, fd) => { 65 | if (err) { 66 | return cb(wrapf('open failed', err, { path })) 67 | } 68 | 69 | const buf = Buffer.alloc(64 * 1024) 70 | fs.read(fd, buf, 0, buf.length, 0, (err) => { 71 | if (err) { 72 | return cb(wrapf('read failed', err, { path })) 73 | } 74 | 75 | fs.close(fd, (err) => { 76 | if (err) { 77 | return cb(wrapf('close failed', err, { path })) 78 | } 79 | 80 | cb(null, buf) 81 | }) 82 | }) 83 | }) 84 | } 85 | ``` 86 | 87 | ## Structured errors 88 | 89 | ```js 90 | const { SError } = require('error') 91 | 92 | class ServerError extends SError {} 93 | class ClientError extends SError {} 94 | 95 | const err = ServerError.create( 96 | '{title} server error, status={statusCode}', { 97 | title: 'some title', 98 | statusCode: 500 99 | } 100 | ) 101 | const err2 = ClientError.create( 102 | '{title} client error, status={statusCode}', { 103 | title: 'some title', 104 | statusCode: 404 105 | } 106 | ) 107 | ``` 108 | 109 | ## Wrapped Errors 110 | 111 | ```js 112 | const net = require('net'); 113 | const { WError } = require('error') 114 | 115 | class ServerListenError extends WError {} 116 | 117 | var server = net.createServer(); 118 | 119 | server.on('error', function onError(err) { 120 | if (err.code === 'EADDRINUSE') { 121 | throw ServerListenFailedError.wrap( 122 | 'error in server, on port={requestPort}', err, { 123 | requestPort: 3000, 124 | host: null 125 | } 126 | ) 127 | } else { 128 | throw err; 129 | } 130 | }); 131 | 132 | server.listen(3000); 133 | ``` 134 | 135 | ## Comparison to Alternatives. 136 | 137 | There are alternative existing libraries for creating typed 138 | and wrapped errors on npm. Here's a quick comparison to some 139 | alternatives. 140 | 141 | ### [`verror`][verror] 142 | 143 | This module takes inspiration from `verror` and adds improvements. 144 | - You can pass extra fields as meta data on the error 145 | - The templating forces dynamic strings to be extra fields. 146 | - Uses ES6 classes for inheritance. This gives your errors unique 147 | class names and makes them show up in heapdumps. 148 | - Has JSON.stringify support 149 | 150 | ### [`error@7.x`][7.x] 151 | 152 | This package used to have a completely different API on the 153 | [7.x][7.x] branch. 154 | - New `error` module uses actual classes instead of dynamically 155 | monkey patching fields onto `new Error()` 156 | - Implementation is more static, previous code was very dynamic 157 | - Simpler API, see the message & properties in one place. 158 | - `wrapf` & `errorf` helpers for less boilerplate. 159 | 160 | ### Hand writing `Error` sub classes. 161 | 162 | You can create your own Error classes by hand. This tends to lead 163 | to 10-20 lines of boilerplate per error which is replace with 164 | one line by using the `error` module; aka 165 | 166 | ```js 167 | class AccountsServerFailureError extends SError {} 168 | class ConnectionResetError extends WError {} 169 | ``` 170 | 171 | ### [`ono`][ono] 172 | 173 | The `ono` package has similar functionality with a different API 174 | - `ono` encourages plain errors instead of custom errors by default 175 | - `error` has zero dependencies 176 | - `error` is only one simple file. `ono` is 10. 177 | - `error` implementation is more static, ono is very dynamic. 178 | 179 | ## Documentation 180 | 181 | This package implements three classes, `WError`; `SError` & 182 | `MultiError` 183 | 184 | You are expected to subclass either `WError` or `SError`; 185 | 186 | - `SError` stands for `Structured Error`; it's an error base 187 | class for adding informational fields to your error beyond 188 | just having a message. 189 | - `WError` stands for `Wrapped Error`; it's an error base 190 | class for when you are wrapping an existing error with more 191 | information. 192 | 193 | The `MultiError` class exists to store an array of errors but 194 | still return a single `Error`; This is useful if your doing 195 | a parallel operation and you want to wait for them all to finish 196 | and do something with all of the failures. 197 | 198 | Some utility functions are also exported: 199 | - `findCauseByName`; See if error or any of it's causes is of 200 | the type name. 201 | - `fullStack`; Take a wrapped error and compute a full stack. 202 | - `wrapf`; Utility function to quickly wrap 203 | - `errorf`; Utility function to quickly create an error 204 | - `getInfo`; Utility function to get the info for any error 205 | object. Calls `err.info()` if the method exists. 206 | 207 | ### `WError` 208 | 209 | Example: 210 | 211 | ```js 212 | class ServerListenError extends WError {} 213 | 214 | ServerListenError.wrap('error in server', err, { 215 | port: 3000 216 | }) 217 | ``` 218 | 219 | When using the `WError` class it's recommended to always call 220 | the static `wrap()` method instead of calling the constructor 221 | directly. 222 | 223 | Example (without cause message): 224 | 225 | ```js 226 | class ApplicationStartupError extends WError {} 227 | 228 | ApplicationStartupError.wrap( 229 | 'Could not start the application cleanly: {reason}', 230 | err, 231 | { 232 | skipCauseMessage: true, 233 | reason: 'Failed to read from disk' 234 | } 235 | ) 236 | ``` 237 | 238 | Setting `skipCauseMessage: true` will not append the cause 239 | error message but still make the cause object available. 240 | 241 | ### `const werr = new WError(message, cause, info)` 242 | 243 | Internal constructor, should pass a `message` string, a `cause` 244 | error and a `info` object (or `null`). 245 | 246 | ### `WError.wrap(msgTmpl, cause, info)` 247 | 248 | `wrap()` method to create error instances. This applies the 249 | [`string-template`][string-template] templating to `msgTmpl` 250 | with `info` as a parameter. 251 | 252 | The `cause` parameter must be an `error` 253 | The `info` parameter is an object or `null`. 254 | 255 | The `info` parameter can contain the field `skipCauseMessage: true` 256 | which will make `WError` not append `: ${causeMessage}` to the 257 | message of the error. 258 | 259 | ### `werr.type` 260 | 261 | The `type` field is the machine readable type for this error. 262 | Always use `err.type` and never `err.message` when trying to 263 | determine what kind of error it is. 264 | 265 | The `type` field is unlikely to change but the `message` field 266 | can change. 267 | 268 | ### `werr.fullType()` 269 | 270 | Calling `fullType` will compute a full type for this error and 271 | any causes that it wraps. This gives you a long `type` string 272 | that's a concat for every wrapped cause. 273 | 274 | ### `werr.cause()` 275 | 276 | Returns the `cause` error. 277 | 278 | ### `werr.info()` 279 | 280 | Returns the `info` object passed on. This is merged with the 281 | info of all `cause` errors up the chain. 282 | 283 | ### `werr.toJSON()` 284 | 285 | The `WError` class implements `toJSON()` so that the JSON 286 | serialization makes sense. 287 | 288 | ### `WError.fullStack(err)` 289 | 290 | This returns a full stack; which is a concatenation of this 291 | stack trace and the stack trace of all causes in the cause chain 292 | 293 | ### `WError.findCauseByName(err, name)` 294 | 295 | Given an err and a name will find if the err or any causes 296 | implement the type of that name. 297 | 298 | This allows you to check if a wrapped `ApplicationError` has 299 | for example a `LevelReadError` or `LevelWriteError` in it's cause 300 | chain and handle database errors differently from all other app 301 | errors. 302 | 303 | ### `SError` 304 | 305 | Example: 306 | 307 | ```js 308 | class LevelReadError extends SError {} 309 | 310 | LevelReadError.create('Could not read key: {key}', { 311 | key: '/some/key' 312 | }) 313 | ``` 314 | 315 | When using the `SError` class it's recommended to always call 316 | the static `create()` method instead of calling the constructor 317 | directly. 318 | 319 | ### `const serr = new SError(message, info)` 320 | 321 | Internal constructor that takes a message string & an info object. 322 | 323 | ### `SError.create(messageTmpl, info)` 324 | 325 | The main way to create error objects, takes a message template 326 | and an info object. 327 | 328 | It will use [string-template][string-template] to apply the 329 | template with the `info` object as a parameter. 330 | 331 | ### `SError.getInfo(error)` 332 | 333 | Static method to `getInfo` on a maybe error. The `error` can 334 | be `null` or `undefined`, it can be a plain `new Error()` or 335 | it can be a structured or wrapped error. 336 | 337 | Will return `err.info()` if it exists, returns `{}` if its `null` 338 | and returns `{ ...err }` if its a plain vanilla error. 339 | 340 | ### `serr.type` 341 | 342 | Returns the type field. The `err.type` field is machine readable. 343 | Always use `err.type` & not `err.message` when trying to compare 344 | errors or do any introspection. 345 | 346 | The `type` field is unlikely to change but the `message` field 347 | can change. 348 | 349 | ### `serr.info()` 350 | 351 | Returns the info object for this error. 352 | 353 | ### `serr.toJSON()` 354 | 355 | This class can JSON serialize cleanly. 356 | 357 | ### `MultiError` 358 | 359 | Example: 360 | 361 | ```js 362 | class FanoutError extends MultiError {} 363 | 364 | function doStuff (filePath, cb) { 365 | fanoutDiskReads(filePath, (errors, fileContents) => { 366 | if (errors && errors.length > 0) { 367 | const err = FanoutError.errorFromList(errors) 368 | return cb(err) 369 | } 370 | 371 | // do stuff with files. 372 | }) 373 | } 374 | ``` 375 | 376 | When using the `MultiError` class it's recommended to always 377 | call the static `errorFromList` method instead of calling the 378 | constructor directly. 379 | 380 | ## Usage from typescript 381 | 382 | The `error` library does not have an `index.d.ts` but does have 383 | full `jsdoc` annotations so it should be typesafe to use. 384 | 385 | You will need to configure your `tsconfig` appropiately ... 386 | 387 | ```json 388 | { 389 | "compilerOptions": { 390 | ... 391 | "allowJs": true, 392 | ... 393 | }, 394 | "include": [ 395 | "src/**/*.js", 396 | "node_modules/error/index.js" 397 | ], 398 | "exclude": [ 399 | "node_modules" 400 | ] 401 | } 402 | ``` 403 | 404 | Typescript does not understand well type source code in 405 | `node_modules` without an `index.d.ts` by default, so you 406 | need to tell it to include the implementation of `error/index.js` 407 | during type checking and to `allowJs` to enable typechecking 408 | js + jsdoc comments. 409 | 410 | ## Installation 411 | 412 | `npm install error` 413 | 414 | ## Contributors 415 | 416 | - Raynos 417 | 418 | ## MIT Licenced 419 | 420 | [eris]: https://github.com/rotisserie/eris/tree/v0.1.0 421 | [pkg-errors]: https://github.com/pkg/errors 422 | [7.x]: https://github.com/Raynos/error/tree/v7.x 423 | [dave]: https://dave.cheney.net/2016/04/27/dont-just-check-errors-handle-them-gracefully 424 | [string-template]: https://github.com/Matt-Esch/string-template 425 | [verror]: https://github.com/joyent/node-verror 426 | [ono]: https://github.com/JS-DevTools/ono 427 | -------------------------------------------------------------------------------- /benchmarks/index.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | /** 4 | * Micro benchmark 5 | */ 6 | const { WError, SError } = require('../index') 7 | 8 | const TypedError = require( 9 | './multidep_modules/error-7.2.1/node_modules/error/typed' 10 | ) 11 | const WrappedError = require( 12 | './multidep_modules/error-7.2.1/node_modules/error/wrapped' 13 | ) 14 | 15 | const WARMUP_LOOP = 5000 16 | const RUN_LOOP = 250 * 1000 17 | 18 | let mode = process.argv[2] 19 | if (!mode) { 20 | mode = 'alloc' 21 | } 22 | 23 | console.log('Running benchmarks', mode) 24 | 25 | class ServerError extends SError {} 26 | class ServerListenError extends WError {} 27 | 28 | const ServerTypedError = TypedError({ 29 | type: 'server.5xx', 30 | message: '{title} server error, status={statusCode}', 31 | title: null, 32 | statusCode: null 33 | }) 34 | 35 | const ServerListenWrappedError = WrappedError({ 36 | message: 'server: {origMessage}', 37 | type: 'server.listen-failed', 38 | requestedPort: null, 39 | host: null 40 | }) 41 | 42 | const out = { 43 | result: null 44 | } 45 | 46 | if (mode === 'alloc') { 47 | allocTypedError(WARMUP_LOOP) 48 | console.log('allocTypedError', allocTypedError(RUN_LOOP)) 49 | 50 | allocWrappedError(WARMUP_LOOP) 51 | console.log('allocWrappedError', allocWrappedError(RUN_LOOP)) 52 | 53 | allocSError(WARMUP_LOOP) 54 | console.log('allocSError', allocSError(RUN_LOOP)) 55 | 56 | allocWError(WARMUP_LOOP) 57 | console.log('allocWError', allocWError(RUN_LOOP)) 58 | } else if (mode === 'stringify') { 59 | stringifyTypedError(WARMUP_LOOP) 60 | console.log('stringifyTypedError', stringifyTypedError(RUN_LOOP)) 61 | 62 | stringifyWrappedError(WARMUP_LOOP) 63 | console.log('stringifyWrappedError', stringifyWrappedError(RUN_LOOP)) 64 | 65 | stringifySError(WARMUP_LOOP) 66 | console.log('stringifySError', stringifySError(RUN_LOOP)) 67 | 68 | stringifyWError(WARMUP_LOOP) 69 | console.log('stringifyWError', stringifyWError(RUN_LOOP)) 70 | } 71 | 72 | function allocTypedError (count) { 73 | const start = Date.now() 74 | for (let i = 0; i < count; i++) { 75 | out.result = ServerTypedError({ 76 | title: 'some title', 77 | statusCode: 500 78 | }) 79 | } 80 | return Date.now() - start 81 | } 82 | function stringifyTypedError (count) { 83 | const start = Date.now() 84 | const err = ServerTypedError({ 85 | title: 'some title', 86 | statusCode: 500 87 | }) 88 | Object.defineProperty(err, 'stack', { 89 | enumerable: true, 90 | configurable: true 91 | }) 92 | for (let i = 0; i < count; i++) { 93 | out.result = JSON.stringify(err) 94 | } 95 | return Date.now() - start 96 | } 97 | 98 | function allocWrappedError (count) { 99 | const start = Date.now() 100 | for (let i = 0; i < count; i++) { 101 | out.result = ServerListenWrappedError( 102 | new Error('EADDRINUSE'), { 103 | requestedPort: 3000, 104 | host: 'localhost' 105 | } 106 | ) 107 | } 108 | return Date.now() - start 109 | } 110 | function stringifyWrappedError (count) { 111 | const start = Date.now() 112 | const err = ServerListenWrappedError( 113 | new Error('EADDRINUSE'), { 114 | requestedPort: 3000, 115 | host: 'localhost' 116 | } 117 | ) 118 | Object.defineProperty(err, 'stack', { 119 | enumerable: true, 120 | configurable: true 121 | }) 122 | for (let i = 0; i < count; i++) { 123 | out.result = JSON.stringify(err) 124 | } 125 | return Date.now() - start 126 | } 127 | 128 | function allocSError (count) { 129 | const start = Date.now() 130 | for (let i = 0; i < count; i++) { 131 | out.result = ServerError.create( 132 | '{title} server error, status={statusCode}', { 133 | title: 'some title', 134 | statusCode: 500 135 | } 136 | ) 137 | } 138 | return Date.now() - start 139 | } 140 | function stringifySError (count) { 141 | const start = Date.now() 142 | const err = ServerError.create( 143 | '{title} server error, status={statusCode}', { 144 | title: 'some title', 145 | statusCode: 500 146 | } 147 | ) 148 | for (let i = 0; i < count; i++) { 149 | out.result = JSON.stringify(err) 150 | } 151 | return Date.now() - start 152 | } 153 | 154 | function allocWError (count) { 155 | const start = Date.now() 156 | for (let i = 0; i < count; i++) { 157 | out.result = ServerListenError.wrap( 158 | 'server', new Error('EADDRINUSE'), { 159 | title: 'some title', 160 | statusCode: 500 161 | } 162 | ) 163 | } 164 | return Date.now() - start 165 | } 166 | function stringifyWError (count) { 167 | const start = Date.now() 168 | const err = ServerListenError.wrap( 169 | 'server', new Error('EADDRINUSE'), { 170 | title: 'some title', 171 | statusCode: 500 172 | } 173 | ) 174 | for (let i = 0; i < count; i++) { 175 | out.result = JSON.stringify(err) 176 | } 177 | return Date.now() - start 178 | } 179 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const assert = require('assert') 4 | 5 | /** @typedef {{ 6 | type?: string; 7 | errno?: string; 8 | syscall?: string; 9 | cause?(): Error; 10 | fullType?(this: CustomError): string; 11 | info?(): Record; 12 | toJSON?(): Record; 13 | } & Error} CustomError 14 | */ 15 | 16 | const nargs = /\{([0-9a-zA-Z_]+)\}/g 17 | const lowerCaseKebabRegex = /([a-z])([0-9A-Z])/g 18 | const upperCaseKebabRegex = /([A-Z])([A-Z])(?=[a-z])/g 19 | 20 | const PLAIN_ERROR_FIELDS = [ 21 | 'code', 22 | 'errno', 23 | 'syscall', 24 | 'status', 25 | 'statusCode', 26 | 'time', 27 | 'hostname', 28 | 'region', 29 | 'requestId', 30 | 'retryable', 31 | 'description', 32 | 'path', 33 | 'actual', 34 | 'expected', 35 | 'operator' 36 | ] 37 | 38 | const EMPTY_OBJECT = {} 39 | /** @type {Map} */ 40 | const typeNameCache = new Map() 41 | /** @type {(o: object, k: string) => unknown} */ 42 | const reflectGet = Reflect.get 43 | 44 | class StructuredError extends Error { 45 | /** 46 | * @param {string} message 47 | * @param {object} info 48 | */ 49 | constructor (message, info) { 50 | super(message) 51 | assert(typeof message === 'string') 52 | assert(info !== null && typeof info === 'object') 53 | 54 | /** @type {string} */ 55 | this.name = this.constructor.name 56 | /** @type {string} */ 57 | this.type = getTypeNameCached(this.name) 58 | /** @type {object} */ 59 | this.__info = info 60 | } 61 | 62 | /** @returns {Record} */ 63 | info () { 64 | return { ...this.__info } 65 | } 66 | 67 | /** @returns {Record} */ 68 | toJSON () { 69 | return { 70 | ...this.__info, 71 | message: this.message, 72 | stack: this.stack, 73 | type: this.type, 74 | name: this.name 75 | } 76 | } 77 | 78 | /** @returns {string} */ 79 | static get type () { 80 | return getTypeNameCached(this.name) 81 | } 82 | 83 | /** 84 | * @param {CustomError | null} error 85 | * @returns {Record} 86 | */ 87 | static getInfo (error) { 88 | if (!error) return {} 89 | if (typeof error.info !== 'function') { 90 | return { ...error } 91 | } 92 | 93 | return error.info() 94 | } 95 | 96 | /** 97 | * @param {string} messageTmpl 98 | * @param {Record} [info] 99 | * @returns {StructuredError} 100 | */ 101 | static create (messageTmpl, info) { 102 | assert(typeof messageTmpl === 'string') 103 | const msg = stringTemplate(messageTmpl, info) 104 | 105 | return new this(msg, info || EMPTY_OBJECT) 106 | } 107 | } 108 | exports.SError = StructuredError 109 | 110 | class WrappedError extends Error { 111 | /** 112 | * @param {string} message 113 | * @param {CustomError} cause 114 | * @param {object} info 115 | */ 116 | constructor (message, cause, info) { 117 | super(message) 118 | assert(typeof message === 'string') 119 | assert(info !== null && typeof info === 'object') 120 | assert(isError(cause)) 121 | 122 | /** @type {string} */ 123 | this.name = this.constructor.name 124 | /** @type {string} */ 125 | this.type = getTypeNameCached(this.name) 126 | /** @type {object} */ 127 | this.__info = info 128 | /** @type {CustomError} */ 129 | this.__cause = cause 130 | } 131 | 132 | /** @returns {string} */ 133 | fullType () { 134 | /** @type {string} */ 135 | let causeType 136 | if (typeof this.__cause.fullType === 'function') { 137 | causeType = this.__cause.fullType() 138 | } else if (this.__cause.type) { 139 | causeType = this.__cause.type 140 | } else if (this.__cause.errno || this.__cause.syscall) { 141 | causeType = 'error.wrapped-io.' + 142 | (this.__cause.syscall || 'unknown') + '.' + 143 | (this.__cause.errno || '') 144 | } else { 145 | causeType = 'error.wrapped-unknown' 146 | } 147 | 148 | return this.type + '~!~' + causeType 149 | } 150 | 151 | /** @returns {CustomError} */ 152 | cause () { 153 | return this.__cause 154 | } 155 | 156 | /** @returns {Record} */ 157 | info () { 158 | return WrappedError.fullInfo(this.cause(), this.__info) 159 | } 160 | 161 | /** @returns {Record} */ 162 | toJSON () { 163 | /** @type {Record} */ 164 | let causeJSON 165 | if (typeof this.__cause.toJSON === 'function') { 166 | causeJSON = this.__cause.toJSON() 167 | } else { 168 | causeJSON = getJSONForPlainError(this.__cause) 169 | } 170 | 171 | if (causeJSON.stack) { 172 | delete causeJSON.stack 173 | } 174 | 175 | return { 176 | ...this.info(), 177 | message: this.message, 178 | stack: this.stack, 179 | type: this.type, 180 | fullType: this.fullType(), 181 | name: this.name, 182 | cause: causeJSON 183 | } 184 | } 185 | 186 | /** @returns {string} */ 187 | static get type () { 188 | return getTypeNameCached(this.name) 189 | } 190 | 191 | /** 192 | * @param {Error} err 193 | * @returns {string} 194 | */ 195 | static fullStack (err) { 196 | return fullStack(err) 197 | } 198 | 199 | /** 200 | * @param {Error} err 201 | * @param {string} name 202 | * @returns {CustomError | null} 203 | */ 204 | static findCauseByName (err, name) { 205 | return findCauseByName(err, name) 206 | } 207 | 208 | /** 209 | * @param {CustomError | null} cause 210 | * @param {object} [info] 211 | * @returns {Record} 212 | */ 213 | static fullInfo (cause, info) { 214 | /** @type {Record | undefined} */ 215 | let existing 216 | if (cause && typeof cause.info === 'function') { 217 | existing = cause.info() 218 | } else if (cause) { 219 | existing = getInfoForPlainError(cause) 220 | } 221 | 222 | if (existing) { 223 | return { ...existing, ...info } 224 | } 225 | 226 | return { ...info } 227 | } 228 | 229 | /** 230 | * @param {string} messageTmpl 231 | * @param {Error} cause 232 | * @param {object} [info] 233 | * @returns {WrappedError} 234 | */ 235 | static wrap (messageTmpl, cause, info) { 236 | assert(typeof messageTmpl === 'string') 237 | assert(isError(cause)) 238 | 239 | let msg = stringTemplate( 240 | messageTmpl, 241 | WrappedError.fullInfo(cause, info) 242 | ) 243 | 244 | if (!info || !reflectGet(info, 'skipCauseMessage')) { 245 | msg = msg + ': ' + cause.message 246 | } 247 | 248 | return new this(msg, cause, info || EMPTY_OBJECT) 249 | } 250 | } 251 | exports.WError = WrappedError 252 | 253 | class MultiError extends Error { 254 | /** 255 | * @param {CustomError[]} errors 256 | */ 257 | constructor (errors) { 258 | assert(Array.isArray(errors)) 259 | assert(errors.length >= 1) 260 | for (const err of errors) { 261 | assert(isError(err)) 262 | } 263 | 264 | let msg = 'First of ' + String(errors.length) 265 | msg += ' error' + (errors.length > 1 ? 's' : '') 266 | msg += ': ' + errors[0].message 267 | 268 | super(msg) 269 | 270 | /** @type {CustomError[]} */ 271 | this.__errors = errors 272 | /** @type {string} */ 273 | this.name = this.constructor.name 274 | /** @type {string} */ 275 | this.type = createTypeStr(this.name) + '--' + 276 | getTypeNameCached(errors[0].name) 277 | } 278 | 279 | /** @returns {CustomError[]} */ 280 | errors () { 281 | return this.__errors.slice() 282 | } 283 | 284 | /** 285 | * @returns {{ 286 | * message: string, 287 | * stack: string, 288 | * type: string, 289 | * name: string, 290 | * errors: object[] 291 | * }} 292 | */ 293 | toJSON () { 294 | /** @type {object[]} */ 295 | const out = [] 296 | for (const e of this.__errors) { 297 | if (typeof e.toJSON === 'function') { 298 | const nestedJSON = e.toJSON() 299 | if (nestedJSON.stack) { 300 | delete nestedJSON.stack 301 | } 302 | out.push(nestedJSON) 303 | } else { 304 | out.push(getJSONForPlainError(e)) 305 | } 306 | } 307 | return { 308 | message: this.message, 309 | stack: this.stack || '', 310 | type: this.type, 311 | name: this.name, 312 | errors: out 313 | } 314 | } 315 | 316 | /** 317 | * @param {Error[]} errors 318 | * @returns {null | Error | MultiError} 319 | */ 320 | static errorFromList (errors) { 321 | assert(Array.isArray(errors)) 322 | 323 | if (errors.length === 0) { 324 | return null 325 | } 326 | if (errors.length === 1) { 327 | assert(isError(errors[0])) 328 | return errors[0] 329 | } 330 | return new this(errors) 331 | } 332 | } 333 | exports.MultiError = MultiError 334 | 335 | /** 336 | * @param {CustomError} err 337 | * @param {string} name 338 | * @returns {CustomError | null} 339 | */ 340 | function findCauseByName (err, name) { 341 | assert(isError(err)) 342 | assert(typeof name === 'string') 343 | assert(name.length > 0) 344 | 345 | /** @type {CustomError | null} */ 346 | let currentErr = err 347 | while (currentErr) { 348 | if (currentErr.name === name) { 349 | return currentErr 350 | } 351 | currentErr = typeof currentErr.cause === 'function' 352 | ? currentErr.cause() : null 353 | } 354 | return null 355 | } 356 | exports.findCauseByName = findCauseByName 357 | 358 | /** 359 | * @param {CustomError} err 360 | * @returns {string} 361 | */ 362 | function fullStack (err) { 363 | assert(isError(err)) 364 | 365 | const stack = err.stack || '' 366 | if (typeof err.cause === 'function') { 367 | return stack + '\nCaused by: ' + fullStack(err.cause()) 368 | } 369 | 370 | return stack || '' 371 | } 372 | exports.fullStack = fullStack 373 | 374 | /** 375 | * @param {CustomError | null} error 376 | * @returns {Record} 377 | */ 378 | function getInfo (error) { 379 | return StructuredError.getInfo(error) 380 | } 381 | exports.getInfo = getInfo 382 | 383 | /** 384 | * @param {string} messageTmpl 385 | * @param {Error} cause 386 | * @param {object} info 387 | * @returns {WrappedError} 388 | */ 389 | function wrapf (messageTmpl, cause, info) { 390 | return WrappedError.wrap(messageTmpl, cause, info) 391 | } 392 | exports.wrapf = wrapf 393 | 394 | /** 395 | * @param {string} messageTmpl 396 | * @param {Record} [info] 397 | * @returns {StructuredError} 398 | */ 399 | function errorf (messageTmpl, info) { 400 | return StructuredError.create(messageTmpl, info) 401 | } 402 | exports.errorf = errorf 403 | 404 | /** 405 | * @param {string} name 406 | * @returns {string} 407 | */ 408 | function getTypeNameCached (name) { 409 | let type = typeNameCache.get(name) 410 | if (type) { 411 | return type 412 | } 413 | 414 | type = createTypeStr(name) 415 | typeNameCache.set(name, type) 416 | return type 417 | } 418 | exports.getTypeName = getTypeNameCached 419 | 420 | /** 421 | * @param {string} name 422 | * @returns {string} 423 | */ 424 | function createTypeStr (name) { 425 | if (name === 'SError') { 426 | return 'structured.error' 427 | } else if (name === 'WError') { 428 | return 'wrapped.error`' 429 | } 430 | 431 | return name 432 | .replace(lowerCaseKebabRegex, '$1.$2') 433 | .replace(upperCaseKebabRegex, '$1.$2') 434 | .toLowerCase() 435 | } 436 | 437 | /** 438 | * @param {CustomError} cause 439 | * @returns {Record} 440 | */ 441 | function getInfoForPlainError (cause) { 442 | /** @type {Record} */ 443 | const info = {} 444 | for (const field of PLAIN_ERROR_FIELDS) { 445 | const v = reflectGet(cause, field) 446 | if (typeof v !== 'undefined') { 447 | info[field] = v 448 | } 449 | } 450 | return info 451 | } 452 | 453 | /** 454 | * @param {Error} err 455 | * @returns {boolean} 456 | */ 457 | function isError (err) { 458 | return Object.prototype.toString.call(err) === '[object Error]' 459 | ? true 460 | : err instanceof Error 461 | } 462 | 463 | /** 464 | * @param {Error} err 465 | * @returns {Record} 466 | */ 467 | function getJSONForPlainError (err) { 468 | const obj = getInfoForPlainError(err) 469 | Object.assign(obj, { 470 | message: err.message, 471 | type: getTypeNameCached(err.name), 472 | name: err.name 473 | }) 474 | return obj 475 | } 476 | 477 | /** 478 | * Taken from https://www.npmjs.com/package/string-template. 479 | * source: https://github.com/Matt-Esch/string-template 480 | */ 481 | /** 482 | * @param {string} string 483 | * @param {Record} [object] 484 | * @returns {string} 485 | */ 486 | function stringTemplate (string, object) { 487 | if (!object) return string 488 | 489 | return string.replace(nargs, function replaceArg ( 490 | /** @type {string} */ match, 491 | /** @type {string} */ word, 492 | /** @type {number} */ index 493 | ) { 494 | if (string[index - 1] === '{' && 495 | string[index + match.length] === '}' 496 | ) { 497 | return word 498 | } else { 499 | const result = word in object ? object[word] : null 500 | if (result === null || result === undefined) { 501 | return '' 502 | } 503 | 504 | return String(result) 505 | } 506 | }) 507 | } 508 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "error", 3 | "version": "10.4.1", 4 | "description": "Custom errors", 5 | "keywords": [], 6 | "author": "Raynos ", 7 | "repository": "git://github.com/Raynos/error.git", 8 | "main": "index", 9 | "homepage": "https://github.com/Raynos/error", 10 | "contributors": [ 11 | { 12 | "name": "Raynos" 13 | } 14 | ], 15 | "bugs": { 16 | "url": "https://github.com/Raynos/error/issues", 17 | "email": "raynos2@gmail.com" 18 | }, 19 | "binDependencies": { 20 | "istanbul": "0.3.13", 21 | "tsdocstandard": "15.2.2", 22 | "type-coverage": "2.4.3", 23 | "typescript": "3.8.3" 24 | }, 25 | "tsdocstandard": { 26 | "ignore": [ 27 | "benchmarks/index.js" 28 | ] 29 | }, 30 | "dependencies": {}, 31 | "devDependencies": { 32 | "@pre-bundled/tape": "5.0.0", 33 | "@types/node": "13.13.4", 34 | "npm-bin-deps": "1.8.2" 35 | }, 36 | "license": "MIT", 37 | "scripts": { 38 | "check": "npr tsc -p .", 39 | "lint": "npr tsdocstandard -v", 40 | "test": "npm run check && npm run lint && node test/index.js && npm run type-coverage", 41 | "type-coverage": "npr type-coverage --detail --strict --ignore-catch --at-least 100", 42 | "travis-test": "npr istanbul cover ./test/index.js && ((cat coverage/lcov.info | coveralls) || exit 0)", 43 | "cover": "npr istanbul cover --report none --print detail ./test/index.js", 44 | "view-cover": "npr istanbul report html && google-chrome ./coverage/index.html" 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /test/index.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | require('./multi.js') 4 | require('./typed.js') 5 | require('./wrapped.js') 6 | -------------------------------------------------------------------------------- /test/multi.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const test = require('@pre-bundled/tape') 4 | 5 | const { MultiError, errorf, WError } = require('../index.js') 6 | 7 | test('a MultiError', function t (assert) { 8 | class FanoutError extends MultiError {} 9 | 10 | const error1 = FanoutError.errorFromList([]) 11 | assert.equal(error1, null) 12 | 13 | const tempError = errorf('one error') 14 | const error2 = FanoutError.errorFromList([tempError]) 15 | assert.ok(error2) 16 | assert.equal(error2, tempError) 17 | assert.equal(error2 && error2.message, 'one error') 18 | 19 | const error3 = new MultiError([tempError]) 20 | assert.ok(error3) 21 | assert.notEqual(error3, tempError) 22 | assert.equal(error3.message, 'First of 1 error: one error') 23 | assert.deepEqual(error3.errors(), [tempError]) 24 | assert.equal(error3.name, 'MultiError') 25 | assert.equal(error3.type, 'multi.error--structured.error') 26 | 27 | assert.equal(JSON.stringify(error3), JSON.stringify({ 28 | message: 'First of 1 error: one error', 29 | stack: error3.stack, 30 | type: 'multi.error--structured.error', 31 | name: 'MultiError', 32 | errors: [{ 33 | message: 'one error', 34 | type: 'structured.error', 35 | name: 'StructuredError' 36 | }] 37 | })) 38 | 39 | class LevelReadError extends WError {} 40 | 41 | const dbErr1 = new Error('DB not open') 42 | const dbErr2 = new Error('DB already closed') 43 | 44 | const wErr1 = LevelReadError.wrap( 45 | 'could not read key: {key}', dbErr1, { 46 | key: 'foo' 47 | } 48 | ) 49 | const wErr2 = LevelReadError.wrap( 50 | 'could not read key: {key}', dbErr2, { 51 | key: 'bar' 52 | } 53 | ) 54 | 55 | const error4 = /** @type {MultiError} */ 56 | (FanoutError.errorFromList([wErr1, wErr2])) 57 | 58 | assert.ok(error4) 59 | assert.equal(error4.message, 60 | 'First of 2 errors: could not read key: foo: DB not open') 61 | assert.deepEqual(error4.errors(), [wErr1, wErr2]) 62 | assert.equal(error4.name, 'FanoutError') 63 | assert.equal(error4.type, 'fanout.error--level.read.error') 64 | 65 | assert.equal(JSON.stringify(error4), JSON.stringify({ 66 | message: 'First of 2 errors: could not read key: foo: ' + 67 | 'DB not open', 68 | stack: error4.stack, 69 | type: 'fanout.error--level.read.error', 70 | name: 'FanoutError', 71 | errors: [{ 72 | key: 'foo', 73 | message: 'could not read key: foo: DB not open', 74 | type: 'level.read.error', 75 | fullType: 'level.read.error~!~error.wrapped-unknown', 76 | name: 'LevelReadError', 77 | cause: { 78 | message: 'DB not open', 79 | type: 'error', 80 | name: 'Error' 81 | } 82 | }, { 83 | key: 'bar', 84 | message: 'could not read key: bar: DB already closed', 85 | type: 'level.read.error', 86 | fullType: 'level.read.error~!~error.wrapped-unknown', 87 | name: 'LevelReadError', 88 | cause: { 89 | message: 'DB already closed', 90 | type: 'error', 91 | name: 'Error' 92 | } 93 | }] 94 | })) 95 | 96 | assert.end() 97 | }) 98 | -------------------------------------------------------------------------------- /test/typed.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const test = require('@pre-bundled/tape') 4 | 5 | const { SError } = require('../index.js') 6 | 7 | test('a server error', function t (assert) { 8 | class Server5XXError extends SError {} 9 | const error = Server5XXError.create( 10 | '{title} server error, status={statusCode}', { 11 | title: 'some title', 12 | statusCode: 500 13 | } 14 | ) 15 | 16 | assert.equal(Server5XXError.type, 'server.5xx.error') 17 | 18 | assert.equal(error.type, 'server.5xx.error') 19 | assert.deepEqual(error.info(), { 20 | statusCode: 500, 21 | title: 'some title' 22 | }) 23 | assert.equal(error.message, 'some title server error, status=500') 24 | assert.equal(error.toString(), 25 | 'Server5XXError: some title server error, status=500') 26 | 27 | assert.deepEqual(error.info(), { 28 | title: 'some title', 29 | statusCode: 500 30 | }) 31 | assert.deepEqual(error.toJSON(), { 32 | message: error.message, 33 | name: error.name, 34 | stack: error.stack, 35 | title: 'some title', 36 | statusCode: 500, 37 | type: error.type 38 | }) 39 | 40 | assert.end() 41 | }) 42 | 43 | test('null fields', function t (assert) { 44 | class NullError extends SError {} 45 | 46 | const e = NullError.create('myError', { 47 | length: 'foo', 48 | buffer: null, 49 | state: null, 50 | expecting: null 51 | }) 52 | assert.equal(e.type, 'null.error') 53 | assert.equal(NullError.type, 'null.error') 54 | 55 | assert.end() 56 | }) 57 | 58 | test('a client error', function t (assert) { 59 | class Client4XXError extends SError {} 60 | 61 | const error2 = Client4XXError.create( 62 | '{title} client error, status={statusCode}', { 63 | title: 'some title', 64 | statusCode: 404 65 | } 66 | ) 67 | 68 | assert.equal(error2.type, 'client.4xx.error') 69 | assert.deepEqual(error2.info(), { 70 | statusCode: 404, 71 | title: 'some title' 72 | }) 73 | assert.equal(error2.message, 'some title client error, status=404') 74 | assert.equal(error2.toString(), 75 | 'Client4XXError: some title client error, status=404') 76 | 77 | assert.deepEqual(error2.info(), { 78 | title: 'some title', 79 | statusCode: 404 80 | }) 81 | assert.deepEqual(error2.toJSON(), { 82 | message: error2.message, 83 | name: error2.name, 84 | stack: error2.stack, 85 | title: 'some title', 86 | statusCode: 404, 87 | type: error2.type 88 | }) 89 | 90 | assert.end() 91 | }) 92 | -------------------------------------------------------------------------------- /test/wrapped.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const test = require('@pre-bundled/tape') 4 | const net = require('net') 5 | 6 | const { WError, getTypeName } = require('../index.js') 7 | 8 | /** @type {(o: object, k: string) => unknown} */ 9 | const reflectGet = Reflect.get 10 | 11 | test('can create a wrapped error', function t (assert) { 12 | class ServerListenFailedError extends WError {} 13 | 14 | /** @type {Error & { code?: string }} */ 15 | const err = new Error('listen EADDRINUSE') 16 | err.code = 'EADDRINUSE' 17 | 18 | const err2 = ServerListenFailedError.wrap( 19 | 'server failed', err, { 20 | requestedPort: 3426, 21 | host: 'localhost' 22 | } 23 | ) 24 | 25 | assert.equal( 26 | ServerListenFailedError.type, 27 | 'server.listen.failed.error' 28 | ) 29 | 30 | assert.equal(err2.message, 'server failed: listen EADDRINUSE') 31 | assert.deepEqual(err2.info(), { 32 | requestedPort: 3426, 33 | host: 'localhost', 34 | code: 'EADDRINUSE' 35 | }) 36 | 37 | assert.equal(err2.cause(), err) 38 | 39 | assert.equal(err2.toString(), 40 | 'ServerListenFailedError: server failed: listen EADDRINUSE') 41 | 42 | assert.equal(JSON.stringify(err2), JSON.stringify({ 43 | code: 'EADDRINUSE', 44 | requestedPort: 3426, 45 | host: 'localhost', 46 | message: 'server failed: listen EADDRINUSE', 47 | stack: err2.stack, 48 | type: 'server.listen.failed.error', 49 | fullType: 'server.listen.failed.error~!~' + 50 | 'error.wrapped-unknown', 51 | name: 'ServerListenFailedError', 52 | cause: { 53 | code: err.code, 54 | message: err.message, 55 | type: 'error', 56 | name: err.name 57 | } 58 | })) 59 | 60 | assert.end() 61 | }) 62 | 63 | test('can create wrapped error with syscall', function t (assert) { 64 | class SyscallError extends WError {} 65 | 66 | /** @type {Error & { code?: string, syscall?: string }} */ 67 | const err = new Error('listen EADDRINUSE') 68 | err.code = 'EADDRINUSE' 69 | err.syscall = 'listen' 70 | 71 | const err2 = SyscallError.wrap( 72 | 'tchannel socket error ({code} from {syscall})', err 73 | ) 74 | 75 | assert.equal(err2.message, 'tchannel socket error ' + 76 | '(EADDRINUSE from listen): listen EADDRINUSE') 77 | assert.deepEqual(err2.info(), { 78 | syscall: 'listen', 79 | code: 'EADDRINUSE' 80 | }) 81 | assert.equal(err2.type, 'syscall.error') 82 | 83 | assert.end() 84 | }) 85 | 86 | test('wrapping with skipCauseMessage', function t (assert) { 87 | class SyscallError extends WError {} 88 | 89 | /** @type {Error & { code?: string, syscall?: string }} */ 90 | const err = new Error('listen EADDRINUSE') 91 | err.code = 'EADDRINUSE' 92 | err.syscall = 'listen' 93 | 94 | const err2 = SyscallError.wrap( 95 | 'tchannel socket error ({code} from {syscall})', err, { 96 | skipCauseMessage: true 97 | } 98 | ) 99 | 100 | assert.equal(err2.message, 'tchannel socket error ' + 101 | '(EADDRINUSE from listen)') 102 | assert.deepEqual(err2.info(), { 103 | syscall: 'listen', 104 | code: 'EADDRINUSE', 105 | skipCauseMessage: true 106 | }) 107 | assert.equal(err2.type, 'syscall.error') 108 | 109 | assert.end() 110 | }) 111 | 112 | test('wrapping twice', function t (assert) { 113 | class ReadError extends WError {} 114 | class DatabaseError extends WError {} 115 | class BusinessError extends WError {} 116 | 117 | const err = BusinessError.wrap( 118 | 'business', DatabaseError.wrap( 119 | 'db', ReadError.wrap('read', new Error('oops')) 120 | ) 121 | ) 122 | assert.ok(err) 123 | 124 | assert.equal(err.message, 'business: db: read: oops') 125 | assert.equal(err.type, 'business.error') 126 | assert.equal(err.fullType(), 'business.error~!~' + 127 | 'database.error~!~' + 128 | 'read.error~!~' + 129 | 'error.wrapped-unknown') 130 | 131 | assert.equal(JSON.stringify(err), JSON.stringify({ 132 | message: 'business: db: read: oops', 133 | stack: err.stack, 134 | type: 'business.error', 135 | fullType: 'business.error~!~database.error~!~' + 136 | 'read.error~!~error.wrapped-unknown', 137 | name: 'BusinessError', 138 | cause: { 139 | message: 'db: read: oops', 140 | type: 'database.error', 141 | fullType: 'database.error~!~' + 142 | 'read.error~!~error.wrapped-unknown', 143 | name: 'DatabaseError', 144 | cause: { 145 | message: 'read: oops', 146 | type: 'read.error', 147 | fullType: 'read.error~!~error.wrapped-unknown', 148 | name: 'ReadError', 149 | cause: { 150 | message: 'oops', 151 | type: 'error', 152 | name: 'Error' 153 | } 154 | } 155 | } 156 | })) 157 | 158 | assert.end() 159 | }) 160 | 161 | test('handles bad recursive strings', function t (assert) { 162 | class ReadError extends WError {} 163 | 164 | const err2 = ReadError.wrap( 165 | 'read: {code}', new Error('hi'), { 166 | code: 'extra {code}' 167 | } 168 | ) 169 | 170 | assert.ok(err2) 171 | assert.equal(err2.message, 'read: extra {code}: hi') 172 | 173 | assert.end() 174 | }) 175 | 176 | test('can wrap real IO errors', function t (assert) { 177 | class ServerListenFailedError extends WError {} 178 | 179 | const otherServer = net.createServer() 180 | otherServer.once('listening', onPortAllocated) 181 | otherServer.listen(0) 182 | 183 | /** @returns {void} */ 184 | function onPortAllocated () { 185 | const addr = /** @type {{port: number}} */ (otherServer.address()) 186 | const port = addr.port 187 | 188 | const server = net.createServer() 189 | server.on('error', onError) 190 | 191 | server.listen(port) 192 | 193 | /** 194 | * @param {Error} cause 195 | * @returns {void} 196 | */ 197 | function onError (cause) { 198 | const err = ServerListenFailedError.wrap( 199 | 'server listen failed', cause, { 200 | host: 'localhost', 201 | requestedPort: port 202 | } 203 | ) 204 | 205 | otherServer.close() 206 | assertOnError(err, cause, port) 207 | } 208 | } 209 | 210 | /** 211 | * @param {ServerListenFailedError} err 212 | * @param {Error} cause 213 | * @param {number} port 214 | * @returns {void} 215 | */ 216 | function assertOnError (err, cause, port) { 217 | assert.ok(err.message.indexOf('server listen failed: ') >= 0) 218 | assert.ok(err.message.indexOf('listen EADDRINUSE') >= 0) 219 | assert.deepEqual(err.info(), { 220 | requestedPort: port, 221 | host: 'localhost', 222 | code: 'EADDRINUSE', 223 | syscall: 'listen', 224 | errno: 'EADDRINUSE' 225 | }) 226 | 227 | assert.equal(err.cause(), cause) 228 | 229 | assert.ok(err.toString().indexOf('ServerListenFailedError: ') >= 0) 230 | assert.ok(err.toString().indexOf('server listen failed: ') >= 0) 231 | assert.ok(err.toString().indexOf('listen EADDRINUSE') >= 0) 232 | 233 | assert.equal(JSON.stringify(err), JSON.stringify({ 234 | code: 'EADDRINUSE', 235 | errno: 'EADDRINUSE', 236 | syscall: 'listen', 237 | host: 'localhost', 238 | requestedPort: port, 239 | message: err.message, 240 | stack: err.stack, 241 | type: 'server.listen.failed.error', 242 | fullType: 'server.listen.failed.error~!~' + 243 | 'error.wrapped-io.listen.EADDRINUSE', 244 | name: 'ServerListenFailedError', 245 | cause: { 246 | code: 'EADDRINUSE', 247 | errno: 'EADDRINUSE', 248 | syscall: 'listen', 249 | message: err.cause().message, 250 | type: 'error', 251 | name: 'Error' 252 | } 253 | })) 254 | 255 | assert.end() 256 | } 257 | }) 258 | 259 | test('can wrap assert errors', function t (assert) { 260 | class TestError extends WError {} 261 | 262 | const err = TestError.wrap('error', createAssertionError()) 263 | assert.deepEqual(Reflect.get(err.cause(), 'actual'), 'a') 264 | 265 | if (err.message === "error: 'a' === 'b'") { 266 | assert.equal(err.message, "error: 'a' === 'b'") 267 | } else { 268 | assert.ok(/[eE]xpected /.test(err.message)) 269 | assert.ok(err.message.includes('strictly equal')) 270 | } 271 | 272 | assert.ok(err.cause().name.includes('AssertionError')) 273 | const operator = reflectGet(err.info(), 'operator') 274 | assert.ok(operator === '===' || operator === 'strictEqual') 275 | 276 | assert.equal(JSON.stringify(err), JSON.stringify({ 277 | code: 'ERR_ASSERTION', 278 | actual: 'a', 279 | expected: 'b', 280 | operator: operator, 281 | message: 'error: ' + err.cause().message, 282 | stack: err.stack, 283 | type: 'test.error', 284 | fullType: 'test.error~!~error.wrapped-unknown', 285 | name: 'TestError', 286 | cause: { 287 | code: 'ERR_ASSERTION', 288 | actual: 'a', 289 | expected: 'b', 290 | operator: reflectGet(err.cause(), 'operator'), 291 | message: err.cause().message, 292 | type: getTypeName(err.cause().name), 293 | name: err.cause().name 294 | } 295 | })) 296 | 297 | assert.end() 298 | }) 299 | 300 | /** @returns {Error} */ 301 | function createAssertionError () { 302 | try { 303 | require('assert').strictEqual('a', 'b') 304 | return new Error('never') 305 | } catch (_err) { 306 | // eslint-disable-next-line @typescript-eslint/no-unsafe-return 307 | return /** @type {Error} */ (_err) 308 | } 309 | } 310 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "types": ["node"], 4 | "target": "es2018", 5 | "lib": ["es2018"], 6 | "noEmit": true, 7 | "module": "commonjs", 8 | "allowJs": true, 9 | "checkJs": true, 10 | "noFallthroughCasesInSwitch": true, 11 | "noImplicitReturns": true, 12 | "noUnusedLocals": true, 13 | "noUnusedParameters": true, 14 | "strict": true, 15 | "baseUrl": "./", 16 | "paths": { 17 | // "@pre-bundled/rimraf": ["./types/pre-bundled__rimraf"], 18 | "@pre-bundled/tape": ["./types/pre-bundled__tape"], 19 | "*" : ["./types/*"] 20 | } 21 | }, 22 | "include": [ 23 | "types/**/*.d.ts", 24 | "index.js", 25 | "test/**/*.js" 26 | ] 27 | } 28 | -------------------------------------------------------------------------------- /types/pre-bundled__tape/index.d.ts: -------------------------------------------------------------------------------- 1 | // TypeScript Version: 3.0 2 | /// 3 | 4 | export = tape; 5 | 6 | /** 7 | * Create a new test with an optional name string and optional opts object. 8 | * cb(t) fires with the new test object t once all preceeding tests have finished. 9 | * Tests execute serially. 10 | */ 11 | declare function tape(name: string | tape.TestOptions, cb: tape.TestCase): void; 12 | declare function tape(name: string, opts: tape.TestOptions, cb: tape.TestCase): void; 13 | declare function tape(cb: tape.TestCase): void; 14 | 15 | declare namespace tape { 16 | interface TestCase { 17 | (test: Test): void; 18 | } 19 | 20 | /** 21 | * Available opts options for the tape function. 22 | */ 23 | interface TestOptions { 24 | skip?: boolean; // See tape.skip. 25 | timeout?: number; // Set a timeout for the test, after which it will fail. See tape.timeoutAfter. 26 | } 27 | 28 | /** 29 | * Options for the createStream function. 30 | */ 31 | interface StreamOptions { 32 | objectMode?: boolean; 33 | } 34 | 35 | /** 36 | * Generate a new test that will be skipped over. 37 | */ 38 | function skip(name: string | TestOptions, cb: TestCase): void; 39 | function skip(name: string): void; 40 | function skip(name: string, opts: TestOptions, cb: TestCase): void; 41 | function skip(cb: TestCase): void; 42 | 43 | /** 44 | * The onFinish hook will get invoked when ALL tape tests have finished right before tape is about to print the test summary. 45 | */ 46 | function onFinish(cb: () => void): void; 47 | 48 | /** 49 | * Like test(name?, opts?, cb) except if you use .only this is the only test case that will run for the entire process, all other test cases using tape will be ignored. 50 | */ 51 | function only(name: string | TestOptions, cb: TestCase): void; 52 | function only(name: string, opts: TestOptions, cb: TestCase): void; 53 | function only(cb: TestCase): void; 54 | 55 | /** 56 | * Create a new test harness instance, which is a function like test(), but with a new pending stack and test state. 57 | */ 58 | function createHarness(): typeof tape; 59 | /** 60 | * Create a stream of output, bypassing the default output stream that writes messages to console.log(). 61 | * By default stream will be a text stream of TAP output, but you can get an object stream instead by setting opts.objectMode to true. 62 | */ 63 | function createStream(opts?: StreamOptions): NodeJS.ReadableStream; 64 | 65 | interface Test { 66 | /** 67 | * Create a subtest with a new test handle st from cb(st) inside the current test. 68 | * cb(st) will only fire when t finishes. 69 | * Additional tests queued up after t will not be run until all subtests finish. 70 | */ 71 | test(name: string, cb: TestCase): void; 72 | test(name: string, opts: TestOptions, cb: TestCase): void; 73 | 74 | /** 75 | * Declare that n assertions should be run. end() will be called automatically after the nth assertion. 76 | * If there are any more assertions after the nth, or after end() is called, they will generate errors. 77 | */ 78 | plan(n: number): void; 79 | 80 | /** 81 | * Declare the end of a test explicitly. 82 | * If err is passed in t.end will assert that it is falsey. 83 | */ 84 | end(err?: unknown): void; 85 | 86 | /** 87 | * Generate a failing assertion with a message msg. 88 | */ 89 | fail(msg?: string): void; 90 | 91 | /** 92 | * Generate a passing assertion with a message msg. 93 | */ 94 | pass(msg?: string): void; 95 | 96 | /** 97 | * Automatically timeout the test after X ms. 98 | */ 99 | timeoutAfter(ms: number): void; 100 | 101 | /** 102 | * Generate an assertion that will be skipped over. 103 | */ 104 | skip(msg?: string): void; 105 | 106 | /** 107 | * Assert that value is truthy with an optional description message msg. 108 | */ 109 | ok(value: unknown, msg?: string): void; 110 | true(value: unknown, msg?: string): void; 111 | assert(value: unknown, msg?: string): void; 112 | 113 | /** 114 | * Assert that value is falsy with an optional description message msg. 115 | */ 116 | notOk(value: unknown, msg?: string): void; 117 | false(value: unknown, msg?: string): void; 118 | notok(value: unknown, msg?: string): void; 119 | 120 | /** 121 | * Assert that err is falsy. 122 | * If err is non-falsy, use its err.message as the description message. 123 | */ 124 | error(err: unknown, msg?: string): void; 125 | ifError(err: unknown, msg?: string): void; 126 | ifErr(err: unknown, msg?: string): void; 127 | iferror(err: unknown, msg?: string): void; 128 | 129 | /** 130 | * Assert that a === b with an optional description msg. 131 | */ 132 | equal(actual: T, expected: T, msg?: string): void; 133 | equals(actual: T, expected: T, msg?: string): void; 134 | isEqual(actual: T, expected: T, msg?: string): void; 135 | is(actual: T, expected: T, msg?: string): void; 136 | strictEqual(actual: T, expected: T, msg?: string): void; 137 | strictEquals(actual: T, expected: T, msg?: string): void; 138 | 139 | /** 140 | * Assert that a !== b with an optional description msg. 141 | */ 142 | notEqual(actual: unknown, expected: unknown, msg?: string): void; 143 | notEquals(actual: unknown, expected: unknown, msg?: string): void; 144 | notStrictEqual(actual: unknown, expected: unknown, msg?: string): void; 145 | notStrictEquals(actual: unknown, expected: unknown, msg?: string): void; 146 | isNotEqual(actual: unknown, expected: unknown, msg?: string): void; 147 | isNot(actual: unknown, expected: unknown, msg?: string): void; 148 | not(actual: unknown, expected: unknown, msg?: string): void; 149 | doesNotEqual(actual: unknown, expected: unknown, msg?: string): void; 150 | isInequal(actual: unknown, expected: unknown, msg?: string): void; 151 | 152 | /** 153 | * Assert that a and b have the same structure and nested values using node's deepEqual() algorithm with strict comparisons (===) on leaf nodes and an optional description msg. 154 | */ 155 | deepEqual(actual: T, expected: T, msg?: string): void; 156 | deepEquals(actual: T, expected: T, msg?: string): void; 157 | isEquivalent(actual: T, expected: T, msg?: string): void; 158 | same(actual: T, expected: T, msg?: string): void; 159 | 160 | /** 161 | * Assert that a and b do not have the same structure and nested values using node's deepEqual() algorithm with strict comparisons (===) on leaf nodes and an optional description msg. 162 | */ 163 | notDeepEqual(actual: unknown, expected: unknown, msg?: string): void; 164 | notEquivalent(actual: unknown, expected: unknown, msg?: string): void; 165 | notDeeply(actual: unknown, expected: unknown, msg?: string): void; 166 | notSame(actual: unknown, expected: unknown, msg?: string): void; 167 | isNotDeepEqual(actual: unknown, expected: unknown, msg?: string): void; 168 | isNotDeeply(actual: unknown, expected: unknown, msg?: string): void; 169 | isNotEquivalent(actual: unknown, expected: unknown, msg?: string): void; 170 | isInequivalent(actual: unknown, expected: unknown, msg?: string): void; 171 | 172 | /** 173 | * Assert that a and b have the same structure and nested values using node's deepEqual() algorithm with loose comparisons (==) on leaf nodes and an optional description msg. 174 | */ 175 | deepLooseEqual(actual: unknown, expected: unknown, msg?: string): void; 176 | looseEqual(actual: unknown, expected: unknown, msg?: string): void; 177 | looseEquals(actual: unknown, expected: unknown, msg?: string): void; 178 | 179 | /** 180 | * Assert that a and b do not have the same structure and nested values using node's deepEqual() algorithm with loose comparisons (==) on leaf nodes and an optional description msg. 181 | */ 182 | notDeepLooseEqual(actual: unknown, expected: unknown, msg?: string): void; 183 | notLooseEqual(actual: unknown, expected: unknown, msg?: string): void; 184 | notLooseEquals(actual: unknown, expected: unknown, msg?: string): void; 185 | 186 | /** 187 | * Assert that the function call fn() throws an exception. 188 | * expected, if present, must be a RegExp or Function, which is used to test the exception object. 189 | */ 190 | throws(fn: () => void, msg?: string): void; 191 | throws(fn: () => void, exceptionExpected: RegExp | typeof Error, msg?: string): void; 192 | 193 | /** 194 | * Assert that the function call fn() does not throw an exception. 195 | */ 196 | doesNotThrow(fn: () => void, msg?: string): void; 197 | doesNotThrow(fn: () => void, exceptionExpected: RegExp | typeof Error, msg?: string): void; 198 | 199 | /** 200 | * Print a message without breaking the tap output. 201 | * (Useful when using e.g. tap-colorize where output is buffered & console.log will print in incorrect order vis-a-vis tap output.) 202 | */ 203 | comment(msg: string): void; 204 | } 205 | } 206 | -------------------------------------------------------------------------------- /types/pre-bundled__tape/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "noEmit": true, 5 | "forceConsistentCasingInFileNames": true, 6 | "baseUrl": "../", 7 | "typeRoots": [ "../" ], 8 | "types": [], 9 | "lib": ["es6"], 10 | "noImplicitAny": true, 11 | "noImplicitThis": true, 12 | "strictNullChecks": true, 13 | "strictFunctionTypes": true 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /types/pre-bundled__tape/tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "dtslint/dtslint.json", 4 | "../../tslint.json" 5 | ], 6 | "rules": { 7 | "unified-signatures": false 8 | } 9 | } 10 | --------------------------------------------------------------------------------