Skip to content
Permalink

Comparing changes

Choose two branches to see what’s changed or to start a new pull request. If you need to, you can also or learn more about diff comparisons.

Open a pull request

Create a new pull request by comparing changes across two branches. If you need to, you can also . Learn more about diff comparisons here.
base repository: ehmicky/log-process-errors
Failed to load repositories. Confirm that selected base ref is valid, then try again.
Loading
base: 9.4.0
Choose a base ref
...
head repository: ehmicky/log-process-errors
Failed to load repositories. Confirm that selected head ref is valid, then try again.
Loading
compare: 10.0.0
Choose a head ref

Commits on Oct 2, 2022

  1. Update changelog

    ehmicky committed Oct 2, 2022
    Copy the full SHA
    7439d99 View commit details
  2. Copy the full SHA
    eaa8286 View commit details
  3. Remove examples

    ehmicky committed Oct 2, 2022
    Copy the full SHA
    bbb34cb View commit details
  4. Remove testing option

    ehmicky committed Oct 2, 2022
    Copy the full SHA
    f000d6e View commit details
  5. Remove colors option

    ehmicky committed Oct 2, 2022
    Copy the full SHA
    5072b29 View commit details
  6. Improve tests

    ehmicky committed Oct 2, 2022
    Copy the full SHA
    f53161d View commit details
  7. Remove pretty printing

    ehmicky committed Oct 2, 2022
    Copy the full SHA
    4490290 View commit details
  8. Move files

    ehmicky committed Oct 2, 2022
    Copy the full SHA
    7c37035 View commit details
  9. Refactoring

    ehmicky committed Oct 2, 2022
    Copy the full SHA
    1ef15a9 View commit details
  10. Remove level option

    ehmicky committed Oct 2, 2022
    Copy the full SHA
    63e7ac0 View commit details
  11. Refactoring

    ehmicky committed Oct 2, 2022
    Copy the full SHA
    755254a View commit details
  12. Remove exitOn

    ehmicky committed Oct 2, 2022
    Copy the full SHA
    2dcf552 View commit details
  13. Improve options

    ehmicky committed Oct 2, 2022
    Copy the full SHA
    e4e77b7 View commit details
  14. Remove semver

    ehmicky committed Oct 2, 2022
    Copy the full SHA
    cc62108 View commit details
  15. Fix cross-realm errors

    ehmicky committed Oct 2, 2022
    Copy the full SHA
    7a83051 View commit details
  16. Refactoring

    ehmicky committed Oct 2, 2022
    Copy the full SHA
    f7bc486 View commit details
  17. Move logic

    ehmicky committed Oct 2, 2022
    Copy the full SHA
    8e4d108 View commit details
  18. Refactoring

    ehmicky committed Oct 2, 2022
    Copy the full SHA
    6e7dc03 View commit details
  19. Simplify logic

    ehmicky committed Oct 2, 2022
    Copy the full SHA
    63f6c9a View commit details
  20. Refactoring

    ehmicky committed Oct 2, 2022
    Copy the full SHA
    3df1b5c View commit details
  21. Improve repeated logic

    ehmicky committed Oct 2, 2022
    Copy the full SHA
    a546bfa View commit details
  22. Improve logic

    ehmicky committed Oct 2, 2022
    Copy the full SHA
    e8baf4c View commit details
  23. Improve limit logic

    ehmicky committed Oct 2, 2022
    Copy the full SHA
    67f3b7c View commit details
  24. Refactoring

    ehmicky committed Oct 2, 2022
    Copy the full SHA
    3be8bd9 View commit details
  25. Refactoring

    ehmicky committed Oct 2, 2022
    Copy the full SHA
    6324ef1 View commit details
  26. Refactoring

    ehmicky committed Oct 2, 2022
    Copy the full SHA
    fde8f85 View commit details
  27. Refactoring

    ehmicky committed Oct 2, 2022
    Copy the full SHA
    1ee9f53 View commit details
  28. Rename file

    ehmicky committed Oct 2, 2022
    Copy the full SHA
    b88539b View commit details
  29. Improve logic

    ehmicky committed Oct 2, 2022
    Copy the full SHA
    551bd8f View commit details
  30. Improve README

    ehmicky committed Oct 2, 2022
    Copy the full SHA
    418caf7 View commit details
  31. Improve README

    ehmicky committed Oct 2, 2022
    Copy the full SHA
    411f8dc View commit details
  32. Update changelog

    ehmicky committed Oct 2, 2022
    Copy the full SHA
    a3fb508 View commit details
  33. Improve messages

    ehmicky committed Oct 2, 2022
    Copy the full SHA
    3a65a15 View commit details
  34. Improve exit logic

    ehmicky committed Oct 2, 2022
    Copy the full SHA
    83a32c6 View commit details
  35. Refactoring

    ehmicky committed Oct 2, 2022
    Copy the full SHA
    4544cb2 View commit details
  36. Fix changelog

    ehmicky committed Oct 2, 2022
    Copy the full SHA
    51da10a View commit details

Commits on Oct 9, 2022

  1. Update README

    ehmicky committed Oct 9, 2022
    Copy the full SHA
    8d170c1 View commit details
  2. Update README

    ehmicky committed Oct 9, 2022
    Copy the full SHA
    2cef12f View commit details
  3. Improve exit

    ehmicky committed Oct 9, 2022
    Copy the full SHA
    419c913 View commit details
  4. Improve exit option

    ehmicky committed Oct 9, 2022
    Copy the full SHA
    e877c92 View commit details
  5. Rename variable

    ehmicky committed Oct 9, 2022
    Copy the full SHA
    ee19eec View commit details
  6. Start improving tests

    ehmicky committed Oct 9, 2022
    Copy the full SHA
    11442bc View commit details
  7. Fix tests

    ehmicky committed Oct 9, 2022
    Copy the full SHA
    1c41a2f View commit details
  8. Fix tests

    ehmicky committed Oct 9, 2022
    Copy the full SHA
    3101ad7 View commit details
  9. Fix tests

    ehmicky committed Oct 9, 2022
    Copy the full SHA
    7d2c007 View commit details
  10. Add tests for exit

    ehmicky committed Oct 9, 2022
    Copy the full SHA
    938aad1 View commit details
  11. Simplify test

    ehmicky committed Oct 9, 2022
    Copy the full SHA
    56053aa View commit details
  12. Move files

    ehmicky committed Oct 9, 2022
    Copy the full SHA
    15d6827 View commit details
  13. Remove test file

    ehmicky committed Oct 9, 2022
    Copy the full SHA
    49bc9ac View commit details
  14. Rename file

    ehmicky committed Oct 9, 2022
    Copy the full SHA
    3bb3a10 View commit details
Showing with 11,995 additions and 21,706 deletions.
  1. +0 −4 .github/workflows/workflow.yml
  2. +142 −0 CHANGELOG.md
  3. +41 −212 README.md
  4. BIN docs/after.png
  5. BIN docs/before.png
  6. BIN docs/error_pretty.png
  7. BIN docs/error_raw.png
  8. +0 −14 examples/README.md
  9. +0 −18 examples/after.js
  10. +0 −14 examples/before.js
  11. +0 −17 examples/colors.js
  12. +0 −28 examples/errors.js
  13. +0 −17 examples/exit.js
  14. +0 −35 examples/level.js
  15. +0 −21 examples/log.js
  16. +0 −25 examples/restore.js
  17. +10,788 −17,863 package-lock.json
  18. +6 −15 package.json
  19. +22 −0 src/error.js
  20. +0 −72 src/error/main.js
  21. +0 −54 src/error/message.js
  22. +0 −48 src/error/print.js
  23. +0 −12 src/error/stack.js
  24. +51 −0 src/events.js
  25. +44 −47 src/exit.js
  26. +0 −54 src/handle/common.js
  27. +0 −61 src/handle/event.js
  28. +0 −26 src/handle/main.js
  29. +0 −90 src/level.js
  30. +13 −35 src/limit.js
  31. +0 −9 src/log.js
  32. +19 −120 src/main.d.ts
  33. +16 −36 src/main.js
  34. +10 −51 src/main.test-d.ts
  35. +41 −0 src/options.js
  36. +0 −74 src/options/main.js
  37. +0 −45 src/options/runners.js
  38. +0 −63 src/options/testing.js
  39. +46 −59 src/repeat.js
  40. +0 −8 src/utils.js
  41. +11 −21 src/warnings.js
  42. +0 −50 test/colors.js
  43. +36 −0 test/error.js
  44. +65 −0 test/events.js
  45. +0 −130 test/exit.js
  46. +46 −0 test/exit/clock.js
  47. +59 −0 test/exit/main.js
  48. +65 −0 test/exit/process.js
  49. +23 −0 test/helpers/cli.js
  50. +7 −0 test/helpers/console.js
  51. +25 −0 test/helpers/error.js
  52. +64 −0 test/helpers/events.js
  53. +0 −46 test/helpers/events/main.js
  54. +0 −19 test/helpers/events/rejection_handled.js
  55. +0 −21 test/helpers/events/uncaught_exception.js
  56. +0 −13 test/helpers/events/unhandled_rejection.js
  57. +0 −26 test/helpers/events/version.js
  58. +0 −28 test/helpers/events/warning.js
  59. +51 −0 test/helpers/exit.js
  60. +0 −67 test/helpers/init.js
  61. +0 −1 test/helpers/level.js
  62. +0 −10 test/helpers/load.js
  63. +0 −63 test/helpers/normalize.js
  64. +15 −0 test/helpers/process.js
  65. +7 −7 test/helpers/remove.js
  66. +0 −28 test/helpers/stack.js
  67. +8 −0 test/helpers/start.js
  68. +0 −38 test/helpers/testing/ava.js
  69. +0 −8 test/helpers/testing/call.js
  70. +0 −21 test/helpers/testing/jasmine.mjs
  71. +0 −19 test/helpers/testing/mocha.js
  72. +0 −28 test/helpers/testing/node_tap.js
  73. +0 −12 test/helpers/testing/options.js
  74. +0 −24 test/helpers/testing/runners.js
  75. +0 −3 test/helpers/testing/stack.js
  76. +0 −26 test/helpers/testing/tape.js
  77. +0 −157 test/level.js
  78. +28 −86 test/limit.js
  79. +0 −89 test/log.js
  80. +45 −0 test/main.js
  81. +0 −22 test/main/emit.js
  82. +0 −85 test/main/init.js
  83. +0 −39 test/main/snapshots/init.js.md
  84. BIN test/main/snapshots/init.js.snap
  85. +16 −0 test/options.js
  86. +0 −4 test/options/ava.js
  87. +0 −59 test/options/options.js
  88. +0 −138 test/options/snapshots/options.js.md
  89. BIN test/options/snapshots/options.js.snap
  90. +0 −634 test/options/snapshots/testing.js.md
  91. BIN test/options/snapshots/testing.js.snap
  92. +0 −66 test/options/testing.js
  93. +84 −13 test/repeat.js
  94. +32 −0 test/snapshots/error.js.md
  95. BIN test/snapshots/error.js.snap
  96. +0 −29 test/snapshots/level.js.md
  97. BIN test/snapshots/level.js.snap
  98. +0 −106 test/snapshots/log.js.md
  99. BIN test/snapshots/log.js.snap
  100. +0 −165 test/snapshots/warnings.js.md
  101. BIN test/snapshots/warnings.js.snap
  102. +69 −58 test/warnings.js
4 changes: 0 additions & 4 deletions .github/workflows/workflow.yml
Original file line number Diff line number Diff line change
@@ -1,9 +1,5 @@
name: Build
on: [push, pull_request]
env:
# This projects has many layers of nested processes making test coverage not
# work correctly
COVERAGE: 'false'
jobs:
combinations:
# git push --tags does not create any commits so should not trigger a new
142 changes: 142 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,145 @@
# 10.0.0

## Package size

The npm package size has been greatly reduced.

## Custom logic

The `log` option was renamed to [`onError`](README.md#onerror). Its arguments
are `(originalError, event)` instead of `(error, level, originalError)`. The
process error `event` is now passed as a second argument instead of being set as
`error.name`.

Before:

```js
logProcessErrors({
log(error) {
if (error.name === 'UncaughtException') {
console.error(error)
}
},
})
```

After:

```js
logProcessErrors({
onError(error, event) {
if (event === 'uncaughtException') {
console.error(error)
}
},
})
```

## Pretty-printing

Errors are not pretty-printed anymore. As a consequence, the `colors` option was
removed too. The [`onError` option](README.md#onerror) can be used instead to
customize how the errors are printed.

## Filtering

The `levels` option was removed. The [`onError` option](README.md#onerror) can
be used for filtering.

Before:

```js
logProcessErrors({
levels: {
warning: 'silent',
},
})
```

After:

```js
logProcessErrors({
onError(error, event) {
if (event !== 'warning') {
console.error(error)
}
},
})
```

## Testing option

The `testing` option was removed. The [`onError` option](README.md#onerror) can
be used instead.

Before:

```js
logProcessErrors({ testing: 'ava' })
```

After:

```js
logProcessErrors({
// Throw the `error` to make the unit test fail while letting other tests run
onError(error) {
throw error
},
})
```

## Process exit

The `exitOn` option was changed from an array of strings to a simpler boolean.
It was also renamed [`exit`](README.md#exit).

The exit is still graceful, i.e. it waits for ongoing tasks to complete, up to 3
seconds. However, if there are none, the process now exits immediately.

Before:

```js
logProcessErrors({ exitOn: [] })
```

After:

```js
logProcessErrors({ exit: false })
```

## Compatibility with other libraries

If other libraries (such as
[Winston](https://github.com/winstonjs/winston/#to-exit-or-not-to-exit),
[Bugsnag](https://docs.bugsnag.com/platforms/javascript/#reporting-unhandled-errors),
etc.) are also listening for process events, they might also try to exit the
process. This created conflicts with this library. This has been fixed by making
the [`exit` option](README.md#exit) default to `false` when process events
listeners already exist.

## Bug fixes

- Fix support for `--unhandled-rejections=strict`
- Do not crash when `error.stack` is `undefined` or `null`
- Support cross-realm errors

## TypeScript

TypeScript types have been simplified.

## Internal

Added 100% test coverage.

# 9.4.0

## Features

- Reduce npm package size

# 9.3.0

## Documentation
253 changes: 41 additions & 212 deletions README.md
Original file line number Diff line number Diff line change
@@ -13,47 +13,21 @@

Show some ❤️ to Node.js process errors.

Node.js prints process errors
([`uncaughtException`](https://nodejs.org/api/process.html#process_event_uncaughtexception),
[`warning`](https://nodejs.org/api/process.html#process_event_warning),
[`unhandledRejection`](https://nodejs.org/api/process.html#process_event_unhandledrejection),
[`rejectionHandled`](https://nodejs.org/api/process.html#process_event_rejectionhandled))
on the console which is very useful. Unfortunately those errors:

- do not show stack traces for
[`warning`](https://nodejs.org/api/process.html#process_event_warning) and
[`rejectionHandled`](https://nodejs.org/api/process.html#process_event_rejectionhandled)
making them hard to debug.
- are inconvenient to [log to an external service](#log).
- are hard to [test](#testing).
- cannot be conditionally skipped.
- are printed each time an error is repeated (except for
[`warning`](https://nodejs.org/api/process.html#process_event_warning)).
- are not human-friendly.

`log-process-errors` fixes all those issues.

Without `log-process-errors`:

![Screenshot before](docs/before.png)

With `log-process-errors`:

![Screenshot after](docs/after.png)
This improves process errors:
[uncaught](https://nodejs.org/api/process.html#process_event_uncaughtexception)
exceptions,
[unhandled](https://nodejs.org/api/process.html#process_event_unhandledrejection)
promises, promises
[handled too late](https://nodejs.org/api/process.html#process_event_rejectionhandled)
and [warnings](https://nodejs.org/api/process.html#process_event_warning).

# Use cases
# Features

- Proper **logging** of process errors in production.
- **Debugging** of process errors in development.
- Automated **testing** of process errors.

# Demo

You can try this library:

- either directly
[in your browser](https://repl.it/@ehmicky/log-process-errors).
- or by executing the [`examples` files](examples/README.md) in a terminal.
- Stack traces for warnings and
[`rejectionHandled`](https://nodejs.org/api/process.html#process_event_rejectionhandled)
- [Single event handler](#onerror) for all process errors
- Ignore [duplicate](#onerror) process errors
- [Process exit](#exit) is graceful and can be prevented

# Install

@@ -84,213 +58,68 @@ not `require()`.
[`options`](#options) `object?`\
_Return value_: `() => void`

Initializes `log-process-errors`. Should be called as early as possible in the
code, before other `import` statements.
Initializes `log-process-errors`.

```js
import logProcessErrors from 'log-process-errors'
logProcessErrors(options)
```

Returns a function that can be fired to restore Node.js default behavior.
The return value restores Node.js default behavior.

```js
import logProcessErrors from 'log-process-errors'

const restore = logProcessErrors(options)
restore()
```

## Options

### log

_Type_: `function(error, level, originalError)`

By default process errors will be logged to the console using `console.error()`,
`console.warn()`, etc.
### exit

This behavior can be overridden with the `log` option. For example to log
process errors with [Winston](https://github.com/winstonjs/winston) instead:

```js
import logProcessErrors from 'log-process-errors'
_Type_: `boolean`

logProcessErrors({
log(error, level, originalError) {
winstonLogger[level](error.stack)
},
})
```

The function's arguments are [`error`](#error), [`level`](#level) and
[`originalError`](#error).

If logging is asynchronous, the function should return a promise (or use
`async`/`await`). This is not necessary if logging is buffered (like
[Winston](https://github.com/winstonjs/winston)).

Duplicate process errors are only logged once (whether the `log` option is
defined or not).

#### error

_Type_: `Error`

The [`log`](#log) and [`level`](#level) options receive as argument an `error`
instance.

This error is generated based on the original process error but with an improved
`name`, `message` and `stack`. However the original process error is still
available as a third argument to [`log`](#log).

##### error.name

_Type_: `string`\
_Value_: [`'UncaughtException'`](https://nodejs.org/api/process.html#process_event_uncaughtexception),
[`'UnhandledRejection'`](https://nodejs.org/api/process.html#process_event_unhandledrejection),
[`'RejectionHandled'`](https://nodejs.org/api/process.html#process_event_rejectionhandled)
or [`'Warning'`](https://nodejs.org/api/process.html#process_event_warning)

##### error.stack

`error` is prettified when using
[`console`](https://nodejs.org/api/console.html#console_console_log_data_args)
Whether to exit the process on
[uncaught exceptions](https://nodejs.org/api/process.html#process_event_uncaughtexception)
or
[`util.inspect()`](https://nodejs.org/api/util.html#util_util_inspect_object_options):

```js
console.log(error)
```

![Error prettified](docs/error_pretty.png)

But not when using `error.stack` instead:

```js
console.log(error.stack)
```

![Error raw](docs/error_raw.png)
[unhandled promises](https://nodejs.org/api/process.html#process_event_unhandledrejection).

### level
This is `false` by default if other libraries are listening to those events, so
they can perform the exit instead. Otherwise, this is `true`.

_Type_: `object`\
_Default_: `{ warning: 'warn', default: 'error' }`
If some tasks are still ongoing, the exit waits for them to complete up to 3
seconds.

Which log level to use.
### onError

Object keys are the error names:
[`uncaughtException`](https://nodejs.org/api/process.html#process_event_uncaughtexception),
[`warning`](https://nodejs.org/api/process.html#process_event_warning),
[`unhandledRejection`](https://nodejs.org/api/process.html#process_event_unhandledrejection),
[`rejectionHandled`](https://nodejs.org/api/process.html#process_event_rejectionhandled)
or `default`.
_Type_: `(error, event) => Promise<void> | void`\
_Default_: `console.error(error)`

Object values are the log level: `'debug'`, `'info'`, `'warn'`, `'error'`,
`'silent'` or `'default'`. It can also be a function using
[`error` as argument](#error) and returning one of those log levels.
Function called once per process error. Duplicate process errors are ignored.

```js
import logProcessErrors from 'log-process-errors'

// Log process errors with Winston instead
logProcessErrors({
level: {
// Use `debug` log level for `uncaughtException` instead of `error`
uncaughtException: 'debug',

// Skip some logs based on a condition
default(error) {
return shouldSkip(error) ? 'silent' : 'default'
},
onError(error, event) {
winstonLogger.error(error.stack)
},
})
```

### exitOn

_Type_: `string[]`\
_Value_: array of [`'uncaughtException'`](https://nodejs.org/api/process.html#process_event_uncaughtexception),
[`'unhandledRejection'`](https://nodejs.org/api/process.html#process_event_unhandledrejection),
[`'rejectionHandled'`](https://nodejs.org/api/process.html#process_event_rejectionhandled)
or [`'warning'`](https://nodejs.org/api/process.html#process_event_warning)\
_Default_: `['uncaughtException', 'unhandledRejection']` for Node `>= 15.0.0`,
`['uncaughtException']` otherwise.

Which process errors should trigger `process.exit(1)`:

- `['uncaughtException', 'unhandledRejection']` is Node.js default behavior
since Node.js `15.0.0`. Before, only
[`uncaughtException`](https://nodejs.org/api/process.html#process_warning_using_uncaughtexception_correctly)
was enabled.
- use `[]` to prevent any `process.exit(1)`. Recommended if your process is
long-running and does not automatically restart on exit.

`process.exit(1)` will only be fired after successfully logging the process
error.

```js
import logProcessErrors from 'log-process-errors'

logProcessErrors({ exitOn: ['uncaughtException', 'unhandledRejection'] })
```

### testing

_Type_: `string`\
_Value_: `'ava'`, `'mocha'`, `'jasmine'`, `'tape'` or `'node_tap'`\
_Default_: `undefined`

When running tests, makes them fail if there are any process errors.

Example with [Ava](https://github.com/avajs/ava):

<!-- eslint-disable import/order, import/first -->

```js
import logProcessErrors from 'log-process-errors'
// Should be initialized before requiring other dependencies
logProcessErrors({ testing: 'ava' })

import test from 'ava'

// Tests will fail because a warning is triggered
test('Example test', (t) => {
process.emitWarning('Example warning')
t.pass()
})
```

To ignore specific process errors, use the [`level` option](#level):

<!-- eslint-disable import/order, import/first -->

```js
import logProcessErrors from 'log-process-errors'
// Should be initialized before requiring other dependencies
logProcessErrors({ testing: 'ava', level: { warning: 'silent' } })

import test from 'ava'

// Tests will not fail because warnings are `silent`
test('Example test', (t) => {
process.emitWarning('Example warning')
t.pass()
})
```
#### error

## colors
_Type_: `Error`

_Type_: `boolean`\
_Default_: `true` if the output is a terminal.
The process error. This is guaranteed to be an error instance.

Colorizes messages.
#### event

```js
import logProcessErrors from 'log-process-errors'
_Type_: `string`

logProcessErrors({ colors: false })
```
Process event name among:
[`'uncaughtException'`](https://nodejs.org/api/process.html#process_event_uncaughtexception),
[`'unhandledRejection'`](https://nodejs.org/api/process.html#process_event_unhandledrejection),
[`'rejectionHandled'`](https://nodejs.org/api/process.html#process_event_rejectionhandled),
[`'warning'`](https://nodejs.org/api/process.html#process_event_warning).

# Related projects

Binary file removed docs/after.png
Binary file not shown.
Binary file removed docs/before.png
Binary file not shown.
Binary file removed docs/error_pretty.png
Binary file not shown.
Binary file removed docs/error_raw.png
Binary file not shown.
14 changes: 0 additions & 14 deletions examples/README.md

This file was deleted.

18 changes: 0 additions & 18 deletions examples/after.js

This file was deleted.

14 changes: 0 additions & 14 deletions examples/before.js

This file was deleted.

17 changes: 0 additions & 17 deletions examples/colors.js

This file was deleted.

28 changes: 0 additions & 28 deletions examples/errors.js

This file was deleted.

17 changes: 0 additions & 17 deletions examples/exit.js

This file was deleted.

35 changes: 0 additions & 35 deletions examples/level.js

This file was deleted.

21 changes: 0 additions & 21 deletions examples/log.js

This file was deleted.

25 changes: 0 additions & 25 deletions examples/restore.js

This file was deleted.

28,651 changes: 10,788 additions & 17,863 deletions package-lock.json

Large diffs are not rendered by default.

21 changes: 6 additions & 15 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "log-process-errors",
"version": "9.4.0",
"version": "10.0.0",
"type": "module",
"exports": "./build/src/main.js",
"main": "./build/src/main.js",
@@ -46,25 +46,16 @@
"test": "test"
},
"dependencies": {
"colors-option": "^4.2.0",
"figures": "^5.0.0",
"filter-obj": "^5.1.0",
"jest-validate": "^29.0.1",
"map-obj": "^5.0.2",
"mem": "^9.0.2",
"semver": "^7.3.7"
"is-plain-obj": "^4.1.0",
"normalize-exception": "^2.6.0",
"set-error-message": "^1.2.0"
},
"devDependencies": {
"@ehmicky/dev-tasks": "^1.0.91",
"@ehmicky/dev-tasks": "^1.0.92",
"@sinonjs/fake-timers": "^9.1.2",
"execa": "^6.1.0",
"has-ansi": "^5.0.1",
"jasmine": "^4.2.1",
"mocha": "^10.0.0",
"semver": "^7.3.8",
"sinon": "^14.0.0",
"strip-ansi": "^7.0.1",
"tap": "^16.0.1",
"tape": "^5.5.3",
"test-each": "^5.4.0"
},
"engines": {
22 changes: 22 additions & 0 deletions src/error.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import normalizeException from 'normalize-exception'
import setErrorMessage from 'set-error-message'

// Normalize error and add the `event` to its `message`
export const getError = function (value, event) {
const error = normalizeException(value)
setErrorMessage(error, `${getMessage(error, event)}${MESSAGES[event]}`)
return error
}

const getMessage = function ({ message }, event) {
return event === 'rejectionHandled'
? message.replace(MESSAGES.unhandledRejection, '')
: message
}

const MESSAGES = {
uncaughtException: '\nThis exception was thrown but not caught.',
unhandledRejection: '\nThis promise was rejected but not handled.',
rejectionHandled: '\nThis promise was rejected and handled too late.',
warning: '',
}
72 changes: 0 additions & 72 deletions src/error/main.js

This file was deleted.

54 changes: 0 additions & 54 deletions src/error/message.js

This file was deleted.

48 changes: 0 additions & 48 deletions src/error/print.js

This file was deleted.

12 changes: 0 additions & 12 deletions src/error/stack.js

This file was deleted.

51 changes: 51 additions & 0 deletions src/events.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import { getError } from './error.js'
import { exitProcess } from './exit.js'
import { isLimited } from './limit.js'
import { isRepeated } from './repeat.js'

export const EVENTS = [
'uncaughtException',
'unhandledRejection',
'rejectionHandled',
'warning',
]

export const handleEvent = async function (
{ event, opts: { onError, exit }, previousEvents },
value,
origin,
) {
const valueA = await resolveValue(value, event)

if (
isDoubleRejection(event, origin) ||
isLimited(valueA, event, previousEvents) ||
isRepeated(valueA, previousEvents)
) {
return
}

const error = getError(valueA, event)
await onError(error, event)
exitProcess(exit, event)
}

// `rejectionHandled` pass a `Promise` as argument. The other events pass the
// main value as is.
const resolveValue = async function (value, event) {
if (event !== 'rejectionHandled') {
return value
}

try {
return await value
} catch (error) {
return error
}
}

// With `--unhandled-rejections=strict`, `unhandledRejection` also emits an
// `uncaughtException` event. We discard it to avoid repetitions.
const isDoubleRejection = function (event, origin) {
return event === 'uncaughtException' && origin === 'unhandledRejection'
}
91 changes: 44 additions & 47 deletions src/exit.js
Original file line number Diff line number Diff line change
@@ -1,61 +1,58 @@
// Do not destructure so tests can stub it
import process from 'process'
import process, { version } from 'process'

// Exit process according to `opts.exitOn` (default: ['uncaughtException']):
// - `uncaughtException`: default behavior of Node.js and recommended by
// Exit process on `uncaughtException` and `unhandledRejection`
// - This is the default behavior of Node.js
// - This is recommended by
// https://nodejs.org/api/process.html#process_warning_using_uncaughtexception_correctly
// - `unhandledRejection`: default behavior of Node.js since 15.0.0
// `process.exit()` unfortunately aborts any current async operations and
// streams are not flushed (including stdout/stderr):
// - https://github.com/nodejs/node/issues/784
// - https://github.com/nodejs/node/issues/6456
// We go around this problem by:
// - await promise returned by `opts.log()`
// - waiting for few seconds (EXIT_TIMEOUT)
// This last one is a hack. We should instead allow `opts.log()` to return a
// stream, and keep track of all unique returned streams. On exit, we should
// then close them and wait for them to flush. We should then always wait for
// process.stdout|stderr as well.
export const exitProcess = function ({ name, opts: { exitOn } }) {
if (!exitOn.includes(name)) {
// This can be disabled or forced with the `exit` option.
// The process exits by default, except if there are other listeners for those
// events
// - This delegates the decision to exit or not to those listeners
// - Since they would need to make that decision as well
// - And they might exit only after some logic is performed first
// - E.g. Winston waits for logging up to 3s before calling
// `process.exit()`
export const exitProcess = function (exit, event) {
if (!shouldExit(exit, event)) {
return
}

// TODO: use `promisify` instead after
// https://github.com/sinonjs/fake-timers/issues/223 is fixed
// TODO: replace with `timers/promises` `setTimeout()` after dropping support
// for Node <15.0.0
setTimeout(() => {
// eslint-disable-next-line unicorn/no-process-exit, n/no-process-exit
process.exit(EXIT_STATUS)
}, EXIT_TIMEOUT)
process.exitCode = EXIT_CODE
setTimeout(forceExitProcess, EXIT_TIMEOUT).unref()
}

const EXIT_TIMEOUT = 3000
const EXIT_STATUS = 1

export const validateExitOn = function ({ exitOn }) {
if (exitOn === undefined) {
return
const shouldExit = function (exit, event) {
if (!isExitEvent(event)) {
return false
}

const invalidEvents = exitOn.filter((name) => !EVENTS.has(name))

if (invalidEvents.length === 0) {
return
if (exit !== undefined) {
return exit
}

throw new Error(
`Invalid option 'exitOn' '${invalidEvents.join(
', ',
)}': must be one of ${EVENTS_ARR.join(', ')}`,
return process.listeners(event).length <= 1
}

const isExitEvent = function (event) {
return (
event === 'uncaughtException' ||
(event === 'unhandledRejection' && hasNewExitBehavior())
)
}

const EVENTS_ARR = [
'uncaughtException',
'unhandledRejection',
'rejectionHandled',
'warning',
]
const EVENTS = new Set(EVENTS_ARR)
// Since Node 15.0.0, `unhandledRejection` makes the process exit too
// TODO: remove after dropping support for Node <15.0.0
const hasNewExitBehavior = function () {
return Number(version.split('.')[0].replace('v', '')) >= NEW_EXIT_MIN_VERSION
}

const NEW_EXIT_MIN_VERSION = 15

// Let tasks complete for a few seconds before forcing the exit
const forceExitProcess = function () {
// eslint-disable-next-line unicorn/no-process-exit, n/no-process-exit
process.exit(EXIT_CODE)
}

export const EXIT_TIMEOUT = 3000
export const EXIT_CODE = 1
54 changes: 0 additions & 54 deletions src/handle/common.js

This file was deleted.

61 changes: 0 additions & 61 deletions src/handle/event.js

This file was deleted.

26 changes: 0 additions & 26 deletions src/handle/main.js

This file was deleted.

90 changes: 0 additions & 90 deletions src/level.js

This file was deleted.

48 changes: 13 additions & 35 deletions src/limit.js
Original file line number Diff line number Diff line change
@@ -1,50 +1,28 @@
import { emitWarning } from 'process'

// We only allow 100 events (per `event.name`) for the global process because:
// - process errors are exceptional and if more than 100 happen, this is
// We only allow 100 events per `event` for the global process because:
// - Process errors are exceptional and if more than 100 happen, this is
// probably due to some infinite recursion.
// - the `repeated` logic should prevent reaching the threshold
// - The `repeated` logic should prevents reaching the threshold
// - `previousEvents` might otherwise take too much memory and/or create a
// memory leak.
// - it prevents infinite recursions if `opts.log|level()` triggers itself an
// - It prevents infinite recursions if `opts.onError()` triggers itself an
// event.
// The `repeated` logic should prevent it most of the times, but it can still
// happen when `[next]Value` is not an `Error` instance and contain dynamic
// content.
export const isLimited = function ({
previousEvents,
mEmitLimitedWarning,
name,
value,
}) {
if (isLimitedWarning({ name, value })) {
// happen when `value` is not an `Error` instance and contain dynamic content
export const isLimited = function (value, event, previousEvents) {
if (previousEvents.length < MAX_EVENTS || isLimitedWarning(event, value)) {
return false
}

const isLimitedEvent = [...previousEvents].length >= MAX_EVENTS

if (isLimitedEvent) {
mEmitLimitedWarning(name)
}

return isLimitedEvent
}

// Notify that limit has been reached with a `warning` event
export const emitLimitedWarning = function (name) {
emitWarning(ERROR_MESSAGE(name), ERROR_NAME, ERROR_CODE)
emitWarning(`${PREFIX} "${event}" until process is restarted.`)
return true
}

// The `warning` itself should not be skipped
const isLimitedWarning = function ({ name, value = {} }) {
return (
name === 'warning' && value.name === ERROR_NAME && value.code === ERROR_CODE
)
const isLimitedWarning = function (event, value) {
return event === 'warning' && value.message.startsWith(PREFIX)
}

const ERROR_MESSAGE = (name) =>
`Cannot log more than ${MAX_EVENTS} '${name}' until process is restarted`
const ERROR_NAME = 'LogProcessErrors'
const ERROR_CODE = 'TooManyErrors'

const MAX_EVENTS = 100
export const MAX_EVENTS = 100
export const PREFIX = `Cannot log more than ${MAX_EVENTS}`
9 changes: 0 additions & 9 deletions src/log.js

This file was deleted.

139 changes: 19 additions & 120 deletions src/main.d.ts
Original file line number Diff line number Diff line change
@@ -1,156 +1,55 @@
/**
* Actual logging level
* Process event name
*/
export type Level = 'debug' | 'info' | 'warn' | 'error'

/**
* Configured logging level
*/
export type LevelOption = Level | 'default' | 'silent'

/**
* Process error's `name`
*/
export type ErrorName =
export type Event =
| 'uncaughtException'
| 'warning'
| 'unhandledRejection'
| 'rejectionHandled'

declare class ProcessError extends Error {
name:
| 'UncaughtException'
| 'UnhandledRejection'
| 'RejectionHandled'
| 'MultipleResolves'
| 'Warning'
}

export type Options = Partial<{
export type Options = {
/**
* By default process errors will be logged to the console using
* `console.error()`, `console.warn()`, etc.
* This behavior can be overridden with the `log` option.
*
* If logging is asynchronous, the function should return a promise (or use
* `async`/`await`). This is not necessary if logging is buffered (like
* [Winston](https://github.com/winstonjs/winston)).
* Prevent exiting the process on
* [uncaught exceptions](https://nodejs.org/api/process.html#process_event_uncaughtexception)
* or
* [unhandled promises](https://nodejs.org/api/process.html#process_event_unhandledrejection).
*
* @default `console.error()`, `console.warn()`, etc.
*
* @example
* ```js
* logProcessErrors({
* log(error, level, originalError) {
* winstonLogger[level](error.stack)
* },
* })
* ```
* @default false
*/
log: (
error: ProcessError,
level: Level,
originalError: Error,
) => Promise<void> | void
readonly exit?: boolean

/**
* Which log level to use.
* Function called once per process error.
* Duplicate process errors are ignored.
*
* @default { warning: 'warn', default: 'error' }
* @default console.error(error)
*
* @example
* ```js
* // Log process errors with Winston instead
* logProcessErrors({
* level: {
* // Use `debug` log level for `uncaughtException` instead of `error`
* uncaughtException: 'debug',
*
* // Skip some logs based on a condition
* default(error) {
* return shouldSkip(error) ? 'silent' : 'default'
* },
* onError(error, event) {
* winstonLogger.error(error.stack)
* },
* })
* ```
*/
level: {
[errorName in ErrorName | 'default']?:
| LevelOption
| ((error: ProcessError) => LevelOption)
}

/**
* Which process errors should trigger `process.exit(1)`:
* - `['uncaughtException', 'unhandledRejection']` is Node.js default
* behavior since Node.js `15.0.0`. Before, only
* [`uncaughtException`](https://nodejs.org/api/process.html#process_warning_using_uncaughtexception_correctly)
* was enabled.
* - use `[]` to prevent any `process.exit(1)`. Recommended if your process
* is long-running and does not automatically restart on exit.
*
* `process.exit(1)` will only be fired after successfully logging the process
* error.
*
* @default `['uncaughtException', 'unhandledRejection']` for Node
* `>= 15.0.0`, `['uncaughtException']` otherwise.
*
* @example
* ```js
* logProcessErrors({ exitOn: ['uncaughtException', 'unhandledRejection'] })
* ```
*/
exitOn: Level[]

/**
* When running tests, makes them fail if there are any process errors.
*
* @default undefined
*
* @example
* ```js
* import logProcessErrors from 'log-process-errors'
* // Should be initialized before requiring other dependencies
* logProcessErrors({ testing: 'ava' })
*
* import test from 'ava'
*
* // Tests will fail because a warning is triggered
* test('Example test', (t) => {
* process.emitWarning('Example warning')
* t.pass()
* })
* ```
*/
testing: 'ava' | 'mocha' | 'jasmine' | 'tape' | 'node_tap'

/**
* Colorizes messages.
*
* @default `true` if the output is a terminal.
*
* @example
* ```js
* logProcessErrors({ colors: false })
* ```
*/
colors: boolean
}>
readonly onError?: (error: Error, event: Event) => Promise<void> | void
}

/**
* Function that can be fired to restore Node.js default behavior.
* Restores Node.js default behavior.
*
* @example
* ```js
* const restore = logProcessErrors(options)
* restore()
* ```
*/
export type Undo = () => void
type Undo = () => void

/**
* Improve how process errors are logged.
* Should be called as early as possible in the code, before other `import`
* statements.
*
* @example
* ```js
52 changes: 16 additions & 36 deletions src/main.js
Original file line number Diff line number Diff line change
@@ -1,57 +1,37 @@
import process from 'process'

import mem from 'mem'

import { EVENTS } from './handle/main.js'
import { emitLimitedWarning } from './limit.js'
import { getOptions } from './options/main.js'
import { EVENTS, handleEvent } from './events.js'
import { getOptions } from './options.js'
import { removeWarningListener, restoreWarningListener } from './warnings.js'

// Add event handling for all process-related errors
export default function logProcessErrors(opts) {
const optsA = getOptions({ opts })

const optsA = getOptions(opts)
removeWarningListener()

const listeners = addListeners({ opts: optsA })

// Do not use `function.bind()` to keep the right `function.name`
const stopLogProcessErrors = () => stopLogging(listeners)
return stopLogProcessErrors
const listeners = addListeners(optsA)
return stopLogProcessErrors.bind(undefined, listeners)
}

const addListeners = function ({ opts }) {
return Object.entries(EVENTS).map(([name, eventFunc]) =>
addListener({ opts, name, eventFunc }),
)
const addListeners = function (opts) {
return EVENTS.map((event) => addListener(event, opts))
}

const addListener = function ({ opts, name, eventFunc }) {
// `previousEvents` is event-name-specific so that if events of a given event
// stopped being emitted, others still are.
// `previousEvents` can take up some memory, but it should be cleaned up
// by `removeListener()`, i.e. once `eventListener` is garbage collected.
const previousEvents = new Set()
// Should only emit the warning once per event name and per `init()`
const mEmitLimitedWarning = mem(emitLimitedWarning)

const eventListener = eventFunc.bind(undefined, {
const addListener = function (event, opts) {
const eventListener = handleEvent.bind(undefined, {
event,
opts,
name,
previousEvents,
mEmitLimitedWarning,
previousEvents: [],
})
process.on(name, eventListener)

return { eventListener, name }
process.on(event, eventListener)
return { eventListener, event }
}

// Remove all event handlers and restore previous `warning` listeners
const stopLogging = function (listeners) {
const stopLogProcessErrors = function (listeners) {
listeners.forEach(removeListener)
restoreWarningListener()
}

const removeListener = function ({ eventListener, name }) {
process.off(name, eventListener)
const removeListener = function ({ eventListener, event }) {
process.off(event, eventListener)
}
61 changes: 10 additions & 51 deletions src/main.test-d.ts
Original file line number Diff line number Diff line change
@@ -5,64 +5,23 @@ import {
expectError,
} from 'tsd'

import logProcessErrors, {
Options,
Level,
LevelOption,
ErrorName,
Undo,
} from './main.js'
import logProcessErrors, { Options, Event } from './main.js'

const undo = logProcessErrors()
expectType<Undo>(undo)
expectType<void>(undo())
expectError(undo(true))

logProcessErrors({})
expectAssignable<Options>({})

expectAssignable<Level>('debug')
expectNotAssignable<Level>('default')
expectNotAssignable<Level>('silent')
expectNotAssignable<Level>('other')
expectAssignable<Event>('warning')
expectNotAssignable<Event>('other')

expectAssignable<LevelOption>('debug')
expectAssignable<LevelOption>('default')
expectAssignable<LevelOption>('silent')
expectNotAssignable<LevelOption>('other')
logProcessErrors({ onError(error: Error, event: Event) {} })
logProcessErrors({ async onError() {} })
expectError(logProcessErrors({ onError: true }))
expectError(logProcessErrors({ onError(error: boolean) {} }))
expectError(logProcessErrors({ onError: () => true }))

expectAssignable<ErrorName>('warning')
expectNotAssignable<ErrorName>('other')

logProcessErrors({ log(error: Error, level: Level, originalError: Error) {} })
logProcessErrors({ async log() {} })
expectError(logProcessErrors({ log: true }))
expectError(logProcessErrors({ log(error: boolean) {} }))
expectError(logProcessErrors({ log: () => true }))

logProcessErrors({ level: {} })
logProcessErrors({ level: { warning: 'info' } })
logProcessErrors({ level: { default: 'info' } })
logProcessErrors({ level: { warning: 'default' } })
logProcessErrors({ level: { warning: 'silent' } })
logProcessErrors({ level: { warning: (error: Error) => 'silent' } })
expectError(logProcessErrors({ level: true }))
expectError(logProcessErrors({ level: { warning: () => true } }))
expectError(logProcessErrors({ level: { warning: 'other' } }))
expectError(logProcessErrors({ level: { other: 'info' } }))
expectError(
logProcessErrors({ level: { warning: (error: boolean) => 'silent' } }),
)

logProcessErrors({ exitOn: [] })
logProcessErrors({ exitOn: ['info', 'debug'] })
expectError(logProcessErrors({ exitOn: true }))
expectError(logProcessErrors({ exitOn: [true] }))
expectError(logProcessErrors({ exitOn: ['other'] }))

logProcessErrors({ testing: 'ava' })
expectError(logProcessErrors({ testing: true }))
expectError(logProcessErrors({ testing: 'other' }))

logProcessErrors({ colors: true })
expectError(logProcessErrors({ colors: 'true' }))
logProcessErrors({ exit: true })
expectError(logProcessErrors({ exit: 'true' }))
41 changes: 41 additions & 0 deletions src/options.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import isPlainObj from 'is-plain-obj'

// Validate options and assign default options
export const getOptions = function (opts = {}) {
if (!isPlainObj(opts)) {
throw new TypeError(`Options must be a plain object: ${opts}`)
}

const { exit, onError = defaultOnError, ...unknownOpts } = opts

validateExit(exit)

if (typeof onError !== 'function') {
throw new TypeError(`Option "onError" must be a function: ${onError}`)
}

validateUnknownOpts(unknownOpts)

return { exit, onError }
}

const validateExit = function (exit) {
if (exit !== undefined && typeof exit !== 'boolean') {
throw new TypeError(`Option "exit" must be a boolean: ${exit}`)
}
}

// `console` should be referenced inside this function, not outside, as user
// might monkey patch it.
const defaultOnError = function (error) {
// eslint-disable-next-line no-restricted-globals, no-console
console.error(error)
}

const validateUnknownOpts = function (unknownOpts) {
const [unknownOpt] = Object.keys(unknownOpts)

if (unknownOpt !== undefined) {
throw new TypeError(`Option "${unknownOpt}" is unknown.`)
}
}
74 changes: 0 additions & 74 deletions src/options/main.js

This file was deleted.

45 changes: 0 additions & 45 deletions src/options/runners.js

This file was deleted.

63 changes: 0 additions & 63 deletions src/options/testing.js

This file was deleted.

105 changes: 46 additions & 59 deletions src/repeat.js
Original file line number Diff line number Diff line change
@@ -1,84 +1,69 @@
import { inspect } from 'util'

// Events with the same `event` are only logged once because:
// - it makes logs clearer
// - it prevents creating too much CPU load or too many microtasks
// - it prevents creating too many logs, which can be expensive if logs are
import { PREFIX } from './limit.js'

// Duplicate errors are only logged once because:
// - It makes logs clearer
// - It prevents creating too much CPU load or too many microtasks
// - It prevents creating too many logs, which can be expensive if logs are
// hosted remotely
// - it prevents infinite recursions if `opts.log|level()` triggers itself an
// - It prevents infinite recursions if `opts.onError()` triggers itself an
// event (while still reporting that event once)
export const isRepeated = function ({ event, previousEvents }) {
const fingerprint = getFingerprint({ event })

const isRepeatedEvent = previousEvents.has(fingerprint)

if (!isRepeatedEvent) {
previousEvents.add(fingerprint)
// `previousEvents`:
// - Is event-specific so that if events of a given event stopped being
// emitted, others still are.
// - Can take up some memory, but it should be cleaned up by
// `removeListener()`, i.e. once `eventListener` is garbage collected.
export const isRepeated = function (value, previousEvents) {
const previousEvent = getPreviousEvent(value)

if (previousEvents.includes(previousEvent)) {
return true
}

return isRepeatedEvent
// eslint-disable-next-line fp/no-mutating-methods
previousEvents.push(previousEvent)
return false
}

// Serialize `event` into a short fingerprint
const getFingerprint = function ({ event }) {
const entries = EVENT_PROPS.map((propName) =>
serializeEntry({ event, propName }),
)
const eventA = Object.assign({}, ...entries)

const fingerprint = JSON.stringify(eventA)

// We truncate fingerprints to prevent consuming too much memory in case some
// `event` properties are huge.
// This introduces higher risk of false positives (see comment below).
// We do not hash as it would be too CPU-intensive if the value is huge.
const fingerprintA = fingerprint.slice(0, FINGERPRINT_MAX_LENGTH)
return fingerprintA
// Serialize `value` into a short fingerprint.
// We truncate fingerprints to prevent consuming too much memory in case `value`
// is big.
// This introduces higher risk of false positives (see comment below).
// We do not hash as it would be too CPU-intensive if the value is big.
const getPreviousEvent = function (value) {
const previousEvent = isErrorInstance(value)
? serializeError(value)
: stableSerialize(value)
return previousEvent.slice(0, FINGERPRINT_MAX_LENGTH)
}

// We do not serialize `name` since this is already `name-wise`
// Key order matters since fingerprint might be truncated: we serialize short
// and non-dynamic values first.
const EVENT_PROPS = ['nextRejected', 'rejected', 'nextValue', 'value']

const FINGERPRINT_MAX_LENGTH = 1e4

const serializeEntry = function ({ event, propName }) {
const value = event[propName]

if (value === undefined) {
return
}

const valueA = serializeValue({ value })
return { [propName]: valueA }
}

const serializeValue = function ({ value }) {
if (value instanceof Error) {
return serializeError(value)
}

return stableSerialize(value)
const isErrorInstance = function (value) {
return Object.prototype.toString.call(value) === '[object Error]'
}

// We do not serialize `error.message` as it may contain dynamic values like
// timestamps. This means errors are only `error.name` + `error.stack`, which
// should be a good fingerprint.
// Also we only keep first 10 callsites in case of infinitely recursive stack.
const serializeError = function ({ name, stack }) {
const stackA = filterErrorStack({ stack })
return `${name}\n${stackA}`
// Also we only keep first 10 call sites in case of infinitely recursive stack.
const serializeError = function ({ name, message, stack }) {
const messageA = String(message).includes(PREFIX) ? `${message}\n` : ''
const stackA = serializeStack(stack)
return `${name}\n${messageA}${stackA}`
}

const filterErrorStack = function ({ stack }) {
return stack
const serializeStack = function (stack) {
return String(stack)
.split('\n')
.filter((line) => STACK_TRACE_LINE_REGEXP.test(line))
.filter(isStackLine)
.slice(0, STACK_TRACE_MAX_LENGTH)
.join('\n')
}

const isStackLine = function (line) {
return STACK_TRACE_LINE_REGEXP.test(line)
}

const STACK_TRACE_LINE_REGEXP = /^\s+at /u
const STACK_TRACE_MAX_LENGTH = 10

@@ -97,3 +82,5 @@ const stableSerialize = function (value) {
}

const INSPECT_OPTS = { getters: true, sorted: true }

const FINGERPRINT_MAX_LENGTH = 1e4
8 changes: 0 additions & 8 deletions src/utils.js

This file was deleted.

32 changes: 11 additions & 21 deletions src/warnings.js
Original file line number Diff line number Diff line change
@@ -6,43 +6,33 @@ import process from 'process'
// Alternative ways to do it would be to ask users to pass `--no-warnings`
// CLI flag or `NODE_NO_WARNINGS=1` environment variable. But this is not as
// developer-friendly.
// This is a noop if `init()` is called several times
export const removeWarningListener = function () {
if (warningListener === undefined) {
return
if (warningListener !== undefined) {
process.off('warning', warningListener)
}

// This will be a noop if `init()` is called several times
process.off('warning', warningListener)
}

// When this module is undone, Node.js default `warning` listener is restored
// Do not restore if there is some user-defined listener, including if
// `init()` was called several times.
export const restoreWarningListener = function () {
if (warningListener === undefined) {
return
}

// Do not restore if there is some user-defined listener, including if
// `init()` was called several times.
if (getWarningListeners().length !== 0) {
return
if (warningListener !== undefined && getWarningListeners().length === 0) {
process.on('warning', warningListener)
}

process.on('warning', warningListener)
}

// We assume the first `warning` listener is the Node.js default one.
// Checking the function itself makes it rely on internal Node.js code, which
// is brittle.
// This can return `undefined` if `--no-warnings` was used
// This can return an empty array if `--no-warnings` is used.
// This needs be done at load time to ensure:
// - we are not catching user-defined listeners
// - this is idempotent, allowing this module to be called several times
const getWarningListener = function () {
return getWarningListeners()[0]
}

// One side effect is that it removes the possibility to use `--*deprecation`
// CLI flags
const getWarningListeners = function () {
return process.listeners('warning')
}

const warningListener = getWarningListener()
const [warningListener] = getWarningListeners()
50 changes: 0 additions & 50 deletions test/colors.js

This file was deleted.

36 changes: 36 additions & 0 deletions test/error.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import test from 'ava'
import { each } from 'test-each'

import { EVENTS, emit, emitValue } from './helpers/events.js'
import { removeProcessListeners } from './helpers/remove.js'
import { startLogging } from './helpers/start.js'

each(EVENTS, ({ title }, eventName) => {
test.serial(
`should normalize errors passed to onError() | ${title}`,
async (t) => {
const { onError, stopLogging } = startLogging()

const message = 'message'
await emitValue(message, eventName)
const [[error]] = onError.args
t.true(error instanceof Error)
t.true(error.message.startsWith(message))

stopLogging()
},
)

test.serial(`should append a description to error | ${title}`, async (t) => {
const { onError, stopLogging } = startLogging()

await emit(eventName)
const [[error]] = onError.args
t.true(error.stack.includes(error.message))
t.snapshot(error.message)

stopLogging()
})
})

removeProcessListeners()
65 changes: 65 additions & 0 deletions test/events.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import test from 'ava'
import sinon from 'sinon'
import { each } from 'test-each'

import { getConsoleStub } from './helpers/console.js'
import { EVENTS, emit, getCallCount } from './helpers/events.js'
import { removeProcessListeners } from './helpers/remove.js'
import { startLogging } from './helpers/start.js'

const consoleStub = getConsoleStub()

each(EVENTS, ({ title }, eventName) => {
test.serial(`should fire opts.onError() | ${title}`, async (t) => {
const { onError, stopLogging } = startLogging()

t.false(onError.called)
await emit(eventName)
t.is(onError.callCount, getCallCount(eventName))
const [error, event] = onError.args[onError.args.length - 1]
t.true(error instanceof Error)
t.is(event, eventName)

stopLogging()
})

test.serial(
`should handle errors in opts.onError() | ${title}`,
async (t) => {
// Ava modifies how uncaught exceptions are handled there
if (eventName === 'uncaughtException') {
return t.pass()
}

const onError = sinon.spy()
const testError = new Error('test')
const { stopLogging } = startLogging({
onError(...args) {
onError(...args)
throw testError
},
})

await emit(eventName)
const [, [error, event]] = onError.args
t.is(error, testError)
t.is(event, 'unhandledRejection')

stopLogging()
},
)

test.serial(`should log on the console by default | ${title}`, async (t) => {
const { stopLogging } = startLogging({ onError: undefined })

t.false(consoleStub.called)
await emit(eventName)
t.is(consoleStub.callCount, getCallCount(eventName))
t.true(consoleStub.args[consoleStub.args.length - 1][0] instanceof Error)

stopLogging()
consoleStub.reset()
})
})

removeProcessListeners()
130 changes: 0 additions & 130 deletions test/exit.js

This file was deleted.

46 changes: 46 additions & 0 deletions test/exit/clock.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import process, { nextTick } from 'process'
import { promisify } from 'util'

import test from 'ava'

// eslint-disable-next-line no-restricted-imports
import { EXIT_TIMEOUT, EXIT_CODE } from '../../src/exit.js'
import { emit } from '../helpers/events.js'
import { startClockLogging } from '../helpers/exit.js'
import { removeProcessListeners } from '../helpers/remove.js'

const pNextTick = promisify(nextTick)

const advanceClock = function (t, clock) {
t.deepEqual(process.exit.args, [])
clock.tick(EXIT_TIMEOUT)
t.deepEqual(process.exit.args, [[EXIT_CODE]])
}

test.serial('call process.exit() after a timeout', async (t) => {
const { clock, stopLogging } = startClockLogging({ exit: true })

await emit('uncaughtException')
advanceClock(t, clock)

stopLogging()
})

test.serial('wait for async onError() before exiting', async (t) => {
const onErrorDuration = 1e5
const { clock, stopLogging } = startClockLogging({
async onError() {
await promisify(setTimeout)(onErrorDuration)
},
exit: true,
})

await emit('uncaughtException')
clock.tick(onErrorDuration)
await pNextTick()
advanceClock(t, clock)

stopLogging()
})

removeProcessListeners()
59 changes: 59 additions & 0 deletions test/exit/main.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import process, { version } from 'process'

import test from 'ava'

// eslint-disable-next-line no-restricted-imports
import { EXIT_CODE } from '../../src/exit.js'
import { emit } from '../helpers/events.js'
import { startExitLogging } from '../helpers/exit.js'
import { removeProcessListeners } from '../helpers/remove.js'

test.serial('exit process if "exit: true"', async (t) => {
const stopLogging = startExitLogging({ exit: true })

await emit('uncaughtException')
t.is(process.exitCode, EXIT_CODE)

stopLogging()
})

test.serial('does not exit process if "exit: false"', async (t) => {
const stopLogging = startExitLogging({ exit: false })

await emit('uncaughtException')
t.is(process.exitCode, undefined)

stopLogging()
})

test.serial('does not exit process if not an exit event', async (t) => {
const stopLogging = startExitLogging({ exit: true })

await emit('warning')
t.is(process.exitCode, undefined)

stopLogging()
})

test.serial(
'does not exit process if unhandledRejection on Node 14',
async (t) => {
const stopLogging = startExitLogging({ exit: true })

await emit('unhandledRejection')
t.not(process.exitCode === EXIT_CODE, version.startsWith('v14.'))

stopLogging()
},
)

test.serial('exit process by default', async (t) => {
const stopLogging = startExitLogging({ exit: undefined })

await emit('uncaughtException')
t.is(process.exitCode, EXIT_CODE)

stopLogging()
})

removeProcessListeners()
65 changes: 65 additions & 0 deletions test/exit/process.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import process from 'process'

import test from 'ava'

// eslint-disable-next-line no-restricted-imports
import { EXIT_CODE } from '../../src/exit.js'
import { emit } from '../helpers/events.js'
import { startProcessLogging } from '../helpers/exit.js'
import { removeProcessListeners } from '../helpers/remove.js'

test.serial(
'does not exit process by default if there are other listeners',
async (t) => {
const stopLogging = startProcessLogging('uncaughtException', {
exit: undefined,
})

await emit('uncaughtException')
t.is(process.exitCode, undefined)

stopLogging()
},
)

test.serial(
'exits process if there are other listeners but "exit: true"',
async (t) => {
const stopLogging = startProcessLogging('uncaughtException', { exit: true })

await emit('uncaughtException')
t.is(process.exitCode, EXIT_CODE)

stopLogging()
},
)

test.serial(
'exits process by default if there are other listeners for other events',
async (t) => {
const stopLogging = startProcessLogging('unhandledRejection', {
exit: undefined,
})

await emit('uncaughtException')
t.is(process.exitCode, EXIT_CODE)

stopLogging()
},
)

test.serial(
'does not exit process by default if there are other listeners for other events but "exit: false"',
async (t) => {
const stopLogging = startProcessLogging('unhandledRejection', {
exit: false,
})

await emit('uncaughtException')
t.is(process.exitCode, undefined)

stopLogging()
},
)

removeProcessListeners()
23 changes: 23 additions & 0 deletions test/helpers/cli.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { argv } from 'process'

// eslint-disable-next-line import/order
import logProcessErrors from 'log-process-errors'
import { emit } from './events.js'

const emitEvent = async function () {
const stopLogging = logProcessErrors({
onError(error) {
// eslint-disable-next-line no-restricted-globals, no-console
console.log(error.message)
},
exit: false,
})

try {
await emit(argv[2])
} finally {
stopLogging()
}
}

emitEvent()
7 changes: 7 additions & 0 deletions test/helpers/console.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import sinon from 'sinon'

// Spy on `console.error()`
export const getConsoleStub = function () {
// eslint-disable-next-line no-restricted-globals
return sinon.stub(console, 'error')
}
25 changes: 25 additions & 0 deletions test/helpers/error.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
export const getError = function () {
return new Error('message')
}

export const getRandomStackError = function () {
// eslint-disable-next-line fp/no-mutating-assign
return Object.assign(getError(), { stack: ` at ${Math.random()}` })
}

export const getRandomMessageError = function () {
return new Error(String(Math.random()))
}

export const getObjectError = function (eventName) {
return eventName === 'warning' ? 'message' : { message: 'message' }
}

export const getInvalidError = function () {
// eslint-disable-next-line fp/no-mutating-assign
return Object.assign(getError(), {
name: undefined,
message: undefined,
stack: undefined,
})
}
64 changes: 64 additions & 0 deletions test/helpers/events.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import { emitWarning } from 'process'
import { promisify } from 'util'

import { getError } from './error.js'

// TODO: replace with `timers/promises` `setImmediate()` after dropping support
// for Node <15.0.0
const pSetImmediate = promisify(setImmediate)

export const emitMany = async function (eventName, length) {
await emitManyValues(getError, eventName, length)
}

export const emitManyValues = async function (getValue, eventName, length) {
await Promise.all(
Array.from({ length }, () => emitValue(getValue(), eventName)),
)
}

export const emit = async function (eventName) {
await emitValue(getError(), eventName)
}

export const emitValue = async function (value, eventName) {
await EVENTS_MAP[eventName](value)
await pSetImmediate()
}

const uncaughtException = function (value) {
setImmediate(() => {
throw value
})
}

const unhandledRejection = function (value) {
// eslint-disable-next-line promise/catch-or-return
Promise.reject(value)
}

const rejectionHandled = async function (value) {
const promise = Promise.reject(value)
await pSetImmediate()
// eslint-disable-next-line promise/prefer-await-to-then
promise.catch(() => {})
}

const warning = function (value) {
emitWarning(value)
}

const EVENTS_MAP = {
uncaughtException,
unhandledRejection,
rejectionHandled,
warning,
}

export const EVENTS = Object.keys(EVENTS_MAP)

// `rejectionHandled` also fires an additional event: the initial
// `unhandledRejection`
export const getCallCount = function (eventName) {
return eventName === 'rejectionHandled' ? 2 : 1
}
46 changes: 0 additions & 46 deletions test/helpers/events/main.js

This file was deleted.

19 changes: 0 additions & 19 deletions test/helpers/events/rejection_handled.js

This file was deleted.

21 changes: 0 additions & 21 deletions test/helpers/events/uncaught_exception.js

This file was deleted.

13 changes: 0 additions & 13 deletions test/helpers/events/unhandled_rejection.js

This file was deleted.

26 changes: 0 additions & 26 deletions test/helpers/events/version.js

This file was deleted.

28 changes: 0 additions & 28 deletions test/helpers/events/warning.js

This file was deleted.

51 changes: 51 additions & 0 deletions test/helpers/exit.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import process from 'process'

import fakeTimers from '@sinonjs/fake-timers'
import sinon from 'sinon'

import { setProcessEvent, unsetProcessEvent } from './process.js'
import { startLogging } from './start.js'

// Start logging and stub `process.exit()`, while a specific process event
// handler is being used
export const startProcessLogging = function (eventName, opts) {
const processHandler = setProcessEvent(eventName)
const stopLogging = startExitLogging(opts)
return stopProcessLogging.bind(
undefined,
eventName,
stopLogging,
processHandler,
)
}

const stopProcessLogging = function (eventName, stopLogging, processHandler) {
stopLogging()
unsetProcessEvent(eventName, processHandler)
}

// Start logging and stub `process.exit()` and `setTimeout()`
export const startClockLogging = function (opts) {
const clock = fakeTimers.install({ toFake: ['setTimeout'] })
const stopLogging = startExitLogging(opts)
const stopLoggingA = stopClockLogging.bind(undefined, stopLogging, clock)
return { clock, stopLogging: stopLoggingA }
}

const stopClockLogging = function (stopLogging, clock) {
stopLogging()
clock.uninstall()
}

// Start logging and stub `process.exit()`
export const startExitLogging = function (opts) {
sinon.stub(process, 'exit')
const { stopLogging } = startLogging(opts)
return stopExitLogging.bind(undefined, stopLogging)
}

const stopExitLogging = function (stopLogging) {
stopLogging()
process.exit.restore()
process.exitCode = undefined
}
67 changes: 0 additions & 67 deletions test/helpers/init.js

This file was deleted.

1 change: 0 additions & 1 deletion test/helpers/level.js

This file was deleted.

10 changes: 0 additions & 10 deletions test/helpers/load.js

This file was deleted.

63 changes: 0 additions & 63 deletions test/helpers/normalize.js

This file was deleted.

15 changes: 15 additions & 0 deletions test/helpers/process.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import process from 'process'

import sinon from 'sinon'

// Spy on process event handlers
export const setProcessEvent = function (eventName) {
const processHandler = sinon.spy()
process.on(eventName, processHandler)
return processHandler
}

// Revert
export const unsetProcessEvent = function (eventName, processHandler) {
process.off(eventName, processHandler)
}
14 changes: 7 additions & 7 deletions test/helpers/remove.js
Original file line number Diff line number Diff line change
@@ -1,16 +1,16 @@
import process from 'process'

import { EVENTS } from './events/main.js'
import { EVENTS } from './events.js'

// Ava sets up process `uncaughtException` and `unhandledRejection` handlers
// which makes testing them harder.
// We keep the default `warning` event listener so we can test it.
export const removeProcessListeners = function () {
EVENTS.forEach(({ eventName }) => {
// We keep the default `warning` event listener so we can test it
if (eventName === 'warning') {
return
}
EVENTS.forEach(removeProcessListener)
}

const removeProcessListener = function (eventName) {
if (eventName !== 'warning') {
process.removeAllListeners(eventName)
})
}
}
28 changes: 0 additions & 28 deletions test/helpers/stack.js

This file was deleted.

8 changes: 8 additions & 0 deletions test/helpers/start.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import logProcessErrors from 'log-process-errors'
import sinon from 'sinon'

export const startLogging = function (opts) {
const onError = sinon.spy()
const stopLogging = logProcessErrors({ onError, exit: false, ...opts })
return { onError, stopLogging }
}
Loading