Skip to content

Commit

Permalink
Add initial code
Browse files Browse the repository at this point in the history
  • Loading branch information
ehmicky committed Oct 15, 2018
1 parent cee041d commit 8768199
Show file tree
Hide file tree
Showing 11 changed files with 281 additions and 6 deletions.
63 changes: 63 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -1 +1,64 @@
[![downloads](https://img.shields.io/npm/dt/on-process-error.svg?logo=npm)](https://www.npmjs.com/package/on-process-error) [![last commit](https://img.shields.io/github/last-commit/autoserver-org/on-process-error.svg?logo=github)](https://github.com/autoserver-org/on-process-error/graphs/contributors) [![license](https://img.shields.io/github/license/autoserver-org/on-process-error.svg?logo=github)](https://www.apache.org/licenses/LICENSE-2.0) [![npm](https://img.shields.io/npm/v/on-process-error.svg?logo=npm)](https://www.npmjs.com/package/on-process-error) [![node](https://img.shields.io/node/v/on-process-error.svg?logo=node.js)](#) [![JavaScript Style Guide](https://img.shields.io/badge/code_style-standard-brightgreen.svg?logo=javascript)](https://standardjs.com) [![eslint-config-standard-prettier-fp](https://img.shields.io/badge/eslint-config--standard--prettier--fp-green.svg?logo=eslint)](https://github.com/autoserver-org/eslint-config-standard-prettier-fp)

Add an event listener to handle any process errors:

- [`uncaughtException`](https://nodejs.org/api/process.html#process_event_uncaughtexception): an exception was thrown and not caught
- [`unhandledRejection`](https://nodejs.org/api/process.html#process_event_unhandledrejection): a promise was rejected and not handled
- [`rejectionHandled`](https://nodejs.org/api/process.html#process_event_rejectionhandled): a promise was rejected and handled too late
- [`multipleResolves`](https://nodejs.org/api/process.html#process_event_multipleresolves): a promise was resolved/rejected twice
- [`warning`](https://nodejs.org/api/process.html#process_event_warning): a warning was produced using [`process.emitWarning()`](https://nodejs.org/api/process.html#process_process_emitwarning_warning_options)

# Usage

<!-- eslint-disable no-unused-vars, node/no-missing-require,
import/no-unresolved, unicorn/filename-case, strict -->

```js
const onProcessError = require('on-process-error')

const undoSetup = onProcessError.setup()
```

When any process errors occur, it will be logged using `console.error()`:

- the message will include detailed information about the error
- for `warning`, `console.warn()` will be used instead.
- for `uncaughtException`, [`process.exit(1)` will be called after
`console.error()`](https://nodejs.org/api/process.html#process_warning_using_uncaughtexception_correctly).

You can undo everything by firing the function returned by
`onProcessError.setup()` (called `undoSetup` in the example above).

# Custom handling

You can override the default behavior by passing a custom function instead.

<!-- eslint-disable no-empty-function, no-unused-vars, node/no-missing-require,
import/no-unresolved, unicorn/filename-case, strict -->

```js
const onProcessError = require('on-process-error')

const undoSetup = onProcessError.setup(
({ eventName, promiseState, promiseValue, error, message }) => {},
)
```

The function's argument is an object with the following properties:

- `eventName` `{string}`: can be `uncaughtException`, `unhandledRejection`,
`rejectionHandled`, `multipleResolves` or `warning`
- `promiseState` `{string}`: whether promise was `resolved` or `rejected`.
For `unhandledRejection`, `rejectionHandled` and `multipleResolves`.
- `promiseValue` `{any}`: value resolved/rejected by the promise.
For `unhandledRejection`, `rejectionHandled` and `multipleResolves`.
- `error` `{error}`:
- can be:
- thrown by `uncaughtException`
- emitted by `warning`. [`error.name`, `error.code` and `error.detail`](https://nodejs.org/api/process.html#process_event_warning)
might be defined.
- rejected by `unhandledRejection`, `rejectionHandled` or
`multipleResolves`'s promise (if the promise was rejected).
- if the error is not an `Error` instance (e.g. if it is a string), it will
be normalized to one using `new Error()`.
- `message` `{string}`: detailed message summing up all of the above.
2 changes: 1 addition & 1 deletion gulp/files.json
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
{
"JAVASCRIPT": ["*.js", "gulp/**/*.js"],
"JAVASCRIPT": ["*.js", "src/**/*.js", "gulp/**/*.js"],
"MARKDOWN": ["*.md"]
}
6 changes: 1 addition & 5 deletions index.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,3 @@
// eslint-disable-next-line filenames/match-exported
'use strict'

// eslint-disable-next-line no-empty-function
const onProcessError = function() {}

module.exports = onProcessError
module.exports = require('./src')
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
"version": "0.1.1",
"main": "index.js",
"files": [
"src",
"!*~"
],
"scripts": {
Expand Down
19 changes: 19 additions & 0 deletions src/default.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
'use strict'

const { exit } = require('process')

// Default event handler
const defaultHandler = function({ eventName, message }) {
const level = eventName === 'warning' ? 'warn' : 'error'
// eslint-disable-next-line no-restricted-globals, no-console
console[level](message)

// See https://nodejs.org/api/process.html#process_warning_using_uncaughtexception_correctly
if (eventName === 'uncaughtException') {
exit(1)
}
}

module.exports = {
defaultHandler,
}
25 changes: 25 additions & 0 deletions src/error.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
'use strict'

// Retrieve `error`
const getError = function({ error, promiseState, promiseValue }) {
// `error` should always be defined for `uncaughtException` and `warning`.
// The other events are promise-based, in which case it should only be defined
// if the promise was rejected
if (promiseState === 'resolved') {
return
}

const errorA = promiseState === 'rejected' ? promiseValue : error

// Throwing `undefined` (`uncaughtException`) or rejecting a promise with
// `undefined` is improper, so we normalize it to an `Error` instance
const errorB = errorA === undefined ? 'undefined' : errorA

// Normalize error if it's not an `Error` (as it should)
const errorC = new Error(`Error: ${errorB}`)
return errorC
}

module.exports = {
getError,
}
35 changes: 35 additions & 0 deletions src/events.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
'use strict'

const { handleEvent } = require('./handle')

// List of all handled events
// Each event must pass its related `error` or `promise` to the generic
// `handleEvent()`
const uncaughtException = function(context, error) {
handleEvent({ ...context, error })
}

const warning = function(context, error) {
handleEvent({ ...context, error })
}

const unhandledRejection = function(context, promiseValue, promise) {
handleEvent({ ...context, promise })
}

const rejectionHandled = function(context, promise) {
handleEvent({ ...context, promise })
}

// eslint-disable-next-line no-inline-comments
const multipleResolves = function(context, type, promise /*, promiseValue */) {
handleEvent({ ...context, promise })
}

module.exports = {
uncaughtException,
warning,
unhandledRejection,
rejectionHandled,
multipleResolves,
}
37 changes: 37 additions & 0 deletions src/handle.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
'use strict'

const { getError } = require('./error')
const { getMessage } = require('./message')

// Generic event handler for all events.
const handleEvent = async function({ handlerFunc, eventName, error, promise }) {
const { promiseState, promiseValue } = await parsePromise({ promise })
const errorA = getError({ error, promiseValue })
const message = getMessage({
eventName,
promiseState,
promiseValue,
error: errorA,
})

handlerFunc({ eventName, promiseState, promiseValue, error: errorA, message })
}

// Retrieve promise's resolved/rejected state and value.
const parsePromise = async function({ promise }) {
// `uncaughtException` and `warning` events do not have `promise`.
if (promise === undefined) {
return {}
}

try {
const promiseValue = await promise
return { promiseState: 'resolved', promiseValue }
} catch (error) {
return { promiseState: 'rejected', promiseValue: error }
}
}

module.exports = {
handleEvent,
}
5 changes: 5 additions & 0 deletions src/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
'use strict'

module.exports = {
...require('./main'),
}
41 changes: 41 additions & 0 deletions src/main.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
'use strict'

const process = require('process')

const { defaultHandler } = require('./default')
const EVENTS = require('./events')

// Add event handling for all process-related errors
const setup = function(handlerFunc = defaultHandler) {
const listeners = addListeners({ handlerFunc })
const removeAll = removeListeners.bind(null, listeners)
return removeAll
}

const addListeners = function({ handlerFunc }) {
return Object.entries(EVENTS).map((eventName, eventFunc) =>
addListener({ handlerFunc, eventName, eventFunc }),
)
}

const addListener = function({ handlerFunc, eventName, eventFunc }) {
const eventListener = eventFunc.bind(null, { handlerFunc, eventName })
process.on(eventName, eventListener)

return { eventListener, eventName }
}

// Remove all event handlers
const removeListeners = function(listeners) {
listeners.forEach(removeListener)
}

const removeListener = function({ eventListener, eventName }) {
// TODO: use `process.off()` instead of `process.removeListener()`
// after dropping Node.js <10 support
process.removeListener(eventName, eventListener)
}

module.exports = {
setup,
}
53 changes: 53 additions & 0 deletions src/message.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
'use strict'

// Retrieve `message` which sums up all information that can be gathered about
// the event.
const getMessage = function({ eventName, promiseState, promiseValue, error }) {
const mainMessage = MAIN_MESSAGES[eventName]
const warningMessage = getWarningMessage({ eventName, error })
const promiseMessage = getPromiseMessage({ promiseState, promiseValue })

const message = [mainMessage, warningMessage, promiseMessage, error]
.filter(part => part !== undefined)
.join('\n')
return message
}

// First line of `message`
const MAIN_MESSAGES = {
uncaughtException: 'Uncaught exception:',
warning: 'Warning:',
unhandledRejection: 'A promise was rejected but not handled:',
rejectionHandled: 'A promise was handled after being already rejected:',
multipleResolves: 'A promise was resolved/rejected multiple times:',
}

// `warning` events use `Error.name|code|detail` in `message`
const getWarningMessage = function({
eventName,
error: { name, code, detail } = {},
}) {
if (eventName !== 'warning') {
return
}

const codeMessage = code === undefined ? '' : ` (${code})`
const detailMessage = detail === undefined ? '' : ` ${detail}`
const warningMessage = `${name}${codeMessage}${detailMessage}`
return warningMessage
}

// `unhandledRejection`, `rejectionHandled` and `multipleResolves` events show
// the promise's resolved/rejected state and value in `message`
const getPromiseMessage = function({ promiseState, promiseValue }) {
if (promiseState === undefined) {
return
}

const value = promiseValue instanceof Error ? 'an error' : promiseValue
return `Promise was ${promiseState} with ${value}`
}

module.exports = {
getMessage,
}

0 comments on commit 8768199

Please sign in to comment.