merge multiple repositories into an existing monorepo

- merged using: 'monorepo_add.sh libraries-o-error:libraries/o-error'
- see https://github.com/shopsys/monorepo-tools
This commit is contained in:
Jakob Ackermann 2021-08-05 08:34:41 +01:00
commit e2f74274c0
No known key found for this signature in database
GPG key ID: 30C56800FCA3828A
18 changed files with 8894 additions and 0 deletions

View file

@ -0,0 +1,22 @@
version: 2.1
orbs:
node: circleci/node@1.1.6
coveralls: coveralls/coveralls@1.0.6
jobs:
build-and-test:
executor:
name: node/default
steps:
- checkout
- node/with-cache:
steps:
- run: npm install
- run: npm run lint
- run: npm run typecheck
- run: npm run test:coverage
- coveralls/upload
workflows:
build-and-test:
jobs:
- build-and-test

View file

@ -0,0 +1,9 @@
root = true
[*]
charset = utf-8
indent_style = space
indent_size = 2
end_of_line = lf
insert_final_newline = true
trim_trailing_whitespace = true

View file

@ -0,0 +1,23 @@
{
"extends": [
"standard",
"plugin:prettier/recommended",
"plugin:mocha/recommended",
"plugin:chai-expect/recommended",
"plugin:chai-friendly/recommended"
],
"env": {
"node": true
},
"parserOptions": {
"ecmaVersion": 2018
},
"overrides": [
{
"files": ["test/**/*.js"],
"env": {
"mocha": true
}
}
]
}

7
libraries/o-error/.gitignore vendored Normal file
View file

@ -0,0 +1,7 @@
dist
.nyc_output
coverage
node_modules/
.npmrc
Dockerfile

View file

@ -0,0 +1,4 @@
{
"semi": false,
"singleQuote": true
}

View file

@ -0,0 +1,3 @@
# @overleaf/o-error History
Please see the [Releases on GitHub](https://github.com/overleaf/o-error/releases) for history.

21
libraries/o-error/LICENSE Normal file
View file

@ -0,0 +1,21 @@
The MIT License
Copyright (c) 2016-2018 Overleaf https://www.overleaf.com
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.

475
libraries/o-error/README.md Normal file
View file

@ -0,0 +1,475 @@
# @overleaf/o-error
[![npm version](https://badge.fury.io/js/%40overleaf%2Fo-error.svg)](https://badge.fury.io/js/%40overleaf%2Fo-error)
[![CircleCI](https://circleci.com/gh/overleaf/o-error.svg?style=svg)](https://circleci.com/gh/overleaf/o-error)
[![Coverage Status](https://coveralls.io/repos/github/overleaf/o-error/badge.svg?branch=master)](https://coveralls.io/github/overleaf/o-error?branch=master)
Light-weight helpers for handling JavaScript Errors in node.js and the browser.
- Get long stack traces across async functions and callbacks with `OError.tag`.
- Easily make custom `Error` subclasses.
- Wrap internal errors, preserving the original errors for logging as `causes`.
- Play nice with error logging services by keeping data in attached `info` objects instead of the error message.
## Table of Contents
<!-- toc -->
- [Long Stack Traces with `OError.tag`](#long-stack-traces-with-oerrortag)
* [The Problem](#the-problem)
* [The Solution](#the-solution)
* [Adding More Info](#adding-more-info)
* [`async`/`await`](#asyncawait)
* [Better Async Stack Traces in Node 12+](#better-async-stack-traces-in-node-12)
* [Caveat: Shared Error Instances](#caveat-shared-error-instances)
- [Create Custom Error Classes](#create-custom-error-classes)
* [Attaching Extra Info](#attaching-extra-info)
* [Wrapping an Internal Error](#wrapping-an-internal-error)
- [OError API Reference](#oerror-api-reference)
* [new OError(message, [info], [cause])](#new-oerrormessage-info-cause)
* [oError.withInfo(info) ⇒ this](#oerrorwithinfoinfo--this)
* [oError.withCause(cause) ⇒ this](#oerrorwithcausecause--this)
* [OError.maxTags : Number](#oerrormaxtags--number)
* [OError.tag(error, [message], [info]) ⇒ Error](#oerrortagerror-message-info--error)
* [OError.getFullInfo(error) ⇒ Object](#oerrorgetfullinfoerror--object)
* [OError.getFullStack(error) ⇒ string](#oerrorgetfullstackerror--string)
- [References](#references)
<!-- tocstop -->
## Long Stack Traces with `OError.tag`
### The Problem
While JavaScript errors have stack traces, they only go back to the start of the latest tick, so they are often not very useful. For example:
```js
const demoDatabase = {
findUser(id, callback) {
process.nextTick(() => {
// return result asynchronously
if (id === 42) {
callback(null, { name: 'Bob' })
} else {
callback(new Error('not found'))
}
})
},
}
function sayHi1(userId, callback) {
demoDatabase.findUser(userId, (err, user) => {
if (err) return callback(err)
callback(null, 'Hi ' + user.name)
})
}
sayHi1(43, (err, result) => {
if (err) {
console.error(err)
} else {
console.log(result)
}
})
```
The resulting error's stack trace doesn't make any mention of our `sayHi1` function; it starts at the `nextTick` built-in:
```
Error: not found
at process.nextTick (repl:8:18)
at process._tickCallback (internal/process/next_tick.js:61:11)
```
In practice, it's often even worse, like
```
DBError: socket connection refused
at someObscureLibraryFunction (...)
at ...
```
### The Solution
Before passing the error to a callback, call the `OError.tag` function to capture a stack trace at the call site:
```js
const OError = require('.')
function sayHi2(userId, callback) {
demoDatabase.findUser(userId, (err, user) => {
if (err) return callback(OError.tag(err))
callback(null, 'Hi ' + user.name)
})
}
sayHi2(43, (err, result) => {
if (err) {
console.error(OError.getFullStack(OError.tag(err)))
} else {
console.log(result)
}
})
```
And use `OError.getFullStack` to reconstruct the full stack, including the tagged errors:
```
Error: not found
at process.nextTick (repl:8:18)
at process._tickCallback (internal/process/next_tick.js:61:11)
TaggedError
at demoDatabase.findUser (repl:3:37)
at process.nextTick (repl:8:9)
at process._tickCallback (internal/process/next_tick.js:61:11)
TaggedError
at sayHi2 (repl:3:46)
at demoDatabase.findUser (repl:3:21)
at process.nextTick (repl:8:9)
at process._tickCallback (internal/process/next_tick.js:61:11)
```
The full stack contains the original error's stack and also the `TaggedError` stacks. There's some redundancy, but it's better to have too much information than too little.
### Adding More Info
You can add more information at each `tag` call site: a message and an `info` object with custom properties.
```js
function sayHi3(userId, callback) {
demoDatabase.findUser(userId, (err, user) => {
if (err) return callback(OError.tag(err, 'failed to find user', { userId }))
callback(null, 'Hi ' + user.name)
})
}
sayHi3(43, (err, result) => {
if (err) {
OError.tag(err, 'failed to say hi')
console.error(OError.getFullStack(err))
console.error(OError.getFullInfo(err))
} else {
console.log(result)
}
})
```
The `OError.getFullInfo` property merges all of the `info`s from the tags together into one object. This logs a full stack trace with `failed to ...` annotations and an `info` object that contains the `userId` that it failed to find:
```
Error: not found
at process.nextTick (repl:8:18)
at process._tickCallback (internal/process/next_tick.js:61:11)
TaggedError: failed to find user
at demoDatabase.findUser (repl:3:37)
at process.nextTick (repl:8:9)
at process._tickCallback (internal/process/next_tick.js:61:11)
TaggedError: failed to say hi
at sayHi3 (repl:3:12)
at demoDatabase.findUser (repl:3:21)
at process.nextTick (repl:8:9)
at process._tickCallback (internal/process/next_tick.js:61:11)
{ userId: 43 }
```
Logging this information (or reporting it to an error monitoring service) hopefully gives you a good start to figuring out what went wrong.
### `async`/`await`
The `OError.tag` approach works with both async/await and callback-oriented code. When using async/await, the pattern is to catch an error, tag it and rethrow:
```js
const promisify = require('util').promisify
demoDatabase.findUserAsync = promisify(demoDatabase.findUser)
async function sayHi4(userId) {
try {
const user = await demoDatabase.findUserAsync(userId)
return `Hi ${user.name}`
} catch (error) {
throw OError.tag(error, 'failed to find user', { userId })
}
}
async function main() {
try {
await sayHi4(43)
} catch (error) {
OError.tag(error, 'failed to say hi')
console.error(OError.getFullStack(error))
console.error(OError.getFullInfo(error))
}
}
main()
```
The resulting full stack trace points to `sayHi4` in `main`, as expected:
```
Error: not found
at process.nextTick (repl:8:18)
at process._tickCallback (internal/process/next_tick.js:61:11)
TaggedError: failed to find user
at sayHi4 (repl:6:18)
at process._tickCallback (internal/process/next_tick.js:68:7)
TaggedError: failed to say hi
at main (repl:5:12)
at process._tickCallback (internal/process/next_tick.js:68:7)
{ userId: 43 }
```
### Better Async Stack Traces in Node 12+
The above output is from node 10. Node 12 has improved stack traces for async code that uses native promises. However, until your whole stack, including all libraries, is using async/await and native promises, you're still likely to get unhelpful stack traces. So, the tagging approach still adds value, even in node 12. (And the `info` from tagging can add value even to a good stack trace, because it can contain clues about the input the caused the error.)
### Caveat: Shared Error Instances
Some libraries, such as `ioredis`, may return the same `Error` instance to multiple callbacks. In this case, the tags may be misleading, because they will be a mixture of the different 'stacks' that lead to the error. You can either accept this or choose to instead wrap the errors from these libraries with new `OError` instances using `withCause`.
In the worst case, a library that always returns a single instance of an error could cause a resource leak. To prevent this, `OError` will only add up to `OError.maxTags` (default 100) tags to a single Error instance.
## Create Custom Error Classes
Broadly speaking, there are two kinds of errors: those we try to recover from, and those for which we give up (i.e. a 5xx response in a web application). For the latter kind, we usually just want to log a message and stack trace useful for debugging, which `OError.tag` helps with.
To recover from an error, we usually need to know what kind of error it was and perhaps to check some of its properties. Defining a custom Error subclass is a good way to do this. Callers can check the type of the error either with `instanceof` or using a custom property, such as `code`.
With ES6 classes, creating a custom error subclass is mostly as simple as `extends Error`. One extra line is required to set the error's `name` appropriately, and inheriting from `OError` handles this implementation detail. Here's an example:
```js
class UserNotFoundError extends OError {
constructor() {
super('user not found')
}
}
try {
throw new UserNotFoundError()
} catch (error) {
console.error(`instanceof Error: ${error instanceof Error}`)
console.error(
`instanceof UserNotFoundError: ${error instanceof UserNotFoundError}`
)
console.error(error.stack)
}
```
```
instanceof Error: true
instanceof UserNotFoundError: true
UserNotFoundError: user not found
at repl:2:9
...
```
### Attaching Extra Info
Whether for helping with error recovery or just for debugging, it is often helpful to include some of the state that caused the error in the error. One way to do this is to put it in the message, but this has a few problems:
- Even if the error is later handled and recovered from, we spend time stringifying the state to add it to the error message.
- Error monitoring systems often look at the message when trying to group similar errors together, and they can get confused by the ever-changing messages.
- When using structured logging, you lose the ability to easily query or filter the logs based on the state; instead clever regexes may be required to get it out of the messages.
Instead, `OError`s (and subclasses) support an `info` object that can contain arbitrary data. Using `info`, we might write the above example as:
```js
class UserNotFoundError extends OError {
constructor(userId) {
super('user not found', { userId })
}
}
try {
throw new UserNotFoundError(123)
} catch (error) {
console.error(OError.getFullStack(error))
console.error(OError.getFullInfo(error))
}
```
```
UserNotFoundError: user not found
at repl:2:9
...
{ userId: 123 }
```
The `OError.getFullInfo` helper merges the `info` on custom errors and any info added with `OError.tag` on its way up the stack. It is intended for use when logging errors. If trying to recover from an error that is known to be a `UserNotFoundError`, it is usually better to interrogate `error.info.userId` directly.
### Wrapping an Internal Error
Detecting a condition like 'user not found' in the example above often starts with an internal database error. It is possible to just let the internal database error propagate all the way up through the stack, but this makes the code more coupled to the internals of the database (or database driver). It is often cleaner to handle and wrap the internal error in one that is under your control. Tying up the examples above:
```js
async function sayHi5(userId) {
try {
const user = await demoDatabase.findUserAsync(userId)
return `Hi ${user.name}`
} catch (error) {
if (error.message === 'not found') {
throw new UserNotFoundError(userId).withCause(error)
}
}
}
async function main() {
try {
await sayHi5(43)
} catch (error) {
OError.tag(error, 'failed to say hi')
console.error(OError.getFullStack(error))
console.error(OError.getFullInfo(error))
}
}
main()
```
The output includes the wrapping error, the tag and the cause, together with the info:
```
UserNotFoundError: user not found
at sayHi5 (repl:7:13)
at process._tickCallback (internal/process/next_tick.js:68:7)
TaggedError: failed to say hi
at main (repl:5:12)
at process._tickCallback (internal/process/next_tick.js:68:7)
caused by:
Error: not found
at process.nextTick (repl:8:18)
at process._tickCallback (internal/process/next_tick.js:61:11)
{ userId: 43 }
```
## OError API Reference
<a name="OError"></a>
* [OError](#OError)
* [new OError(message, [info], [cause])](#new_OError_new)
* _instance_
* [.withInfo(info)](#OError+withInfo) ⇒ <code>this</code>
* [.withCause(cause)](#OError+withCause) ⇒ <code>this</code>
* _static_
* [.maxTags](#OError.maxTags) : <code>Number</code>
* [.tag(error, [message], [info])](#OError.tag) ⇒ <code>Error</code>
* [.getFullInfo(error)](#OError.getFullInfo) ⇒ <code>Object</code>
* [.getFullStack(error)](#OError.getFullStack) ⇒ <code>string</code>
<a name="new_OError_new"></a>
### new OError(message, [info], [cause])
| Param | Type | Description |
| --- | --- | --- |
| message | <code>string</code> | as for built-in Error |
| [info] | <code>Object</code> | extra data to attach to the error |
| [cause] | <code>Error</code> | the internal error that caused this error |
<a name="OError+withInfo"></a>
### oError.withInfo(info) ⇒ <code>this</code>
Set the extra info object for this error.
**Kind**: instance method of [<code>OError</code>](#OError)
| Param | Type | Description |
| --- | --- | --- |
| info | <code>Object</code> | extra data to attach to the error |
<a name="OError+withCause"></a>
### oError.withCause(cause) ⇒ <code>this</code>
Wrap the given error, which caused this error.
**Kind**: instance method of [<code>OError</code>](#OError)
| Param | Type | Description |
| --- | --- | --- |
| cause | <code>Error</code> | the internal error that caused this error |
<a name="OError.maxTags"></a>
### OError.maxTags : <code>Number</code>
Maximum number of tags to apply to any one error instance. This is to avoid
a resource leak in the (hopefully unlikely) case that a singleton error
instance is returned to many callbacks. If tags have been dropped, the full
stack trace will include a placeholder tag `... dropped tags`.
Defaults to 100. Must be at least 1.
**Kind**: static property of [<code>OError</code>](#OError)
<a name="OError.tag"></a>
### OError.tag(error, [message], [info]) ⇒ <code>Error</code>
Tag debugging information onto any error (whether an OError or not) and
return it.
**Kind**: static method of [<code>OError</code>](#OError)
**Returns**: <code>Error</code> - the modified `error` argument
| Param | Type | Description |
| --- | --- | --- |
| error | <code>Error</code> | the error to tag |
| [message] | <code>string</code> | message with which to tag `error` |
| [info] | <code>Object</code> | extra data with wich to tag `error` |
**Example** *(An error in a callback)*
```js
function findUser(name, callback) {
fs.readFile('/etc/passwd', (err, data) => {
if (err) return callback(OError.tag(err, 'failed to read passwd'))
// ...
})
}
```
**Example** *(A possible error in a callback)*
```js
function cleanup(callback) {
fs.unlink('/tmp/scratch', (err) => callback(err && OError.tag(err)))
}
```
**Example** *(An error with async/await)*
```js
async function cleanup() {
try {
await fs.promises.unlink('/tmp/scratch')
} catch (err) {
throw OError.tag(err, 'failed to remove scratch file')
}
}
```
<a name="OError.getFullInfo"></a>
### OError.getFullInfo(error) ⇒ <code>Object</code>
The merged info from any `tag`s and causes on the given error.
If an info property is repeated, the last one wins.
**Kind**: static method of [<code>OError</code>](#OError)
| Param | Type | Description |
| --- | --- | --- |
| error | <code>Error</code> \| <code>null</code> \| <code>undefined</code> | any error (may or may not be an `OError`) |
<a name="OError.getFullStack"></a>
### OError.getFullStack(error) ⇒ <code>string</code>
Return the `stack` property from `error`, including the `stack`s for any
tagged errors added with `OError.tag` and for any `cause`s.
**Kind**: static method of [<code>OError</code>](#OError)
| Param | Type | Description |
| --- | --- | --- |
| error | <code>Error</code> \| <code>null</code> \| <code>undefined</code> | any error (may or may not be an `OError`) |
<!-- END API REFERENCE -->
## References
- [MDN: Error](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Error)
- [Error Handling in Node.js](https://www.joyent.com/node-js/production/design/errors)
- [verror](https://github.com/joyent/node-verror)
- [Custom JavaScript Errors in ES6](https://medium.com/@xjamundx/custom-javascript-errors-in-es6-aa891b173f87)
- [Custom errors, extending Error](https://javascript.info/custom-errors)
- https://gist.github.com/justmoon/15511f92e5216fa2624b (some tests are based largely on this gist)

View file

@ -0,0 +1,9 @@
{
"presets": [
["@babel/env", {
"targets": ["last 1 year", "ie 11", "firefox esr"],
"useBuiltIns": "usage",
"corejs": { "version": 3 }
}]
]
}

View file

@ -0,0 +1,44 @@
//
// A quick microbenchmark for OError.tag.
//
const OError = require('..')
function benchmark(fn, repeats = 100000) {
const startTime = process.hrtime()
for (let i = 0; i < repeats; ++i) {
fn()
}
const elapsed = process.hrtime(startTime)
return elapsed[0] * 1e3 + elapsed[1] * 1e-6
}
function throwError() {
throw new Error('here is a test error')
}
console.log(
'no tagging: ',
benchmark(() => {
try {
throwError()
return 1
} catch (error) {
return 0
}
}),
'ms'
)
console.log(
'tagging: ',
benchmark(() => {
try {
throwError()
return 1
} catch (error) {
OError.tag(error, 'here is a test tag')
return 0
}
}),
'ms'
)

View file

@ -0,0 +1,141 @@
// This is the code from the README.
const OError = require('..')
const demoDatabase = {
findUser(id, callback) {
process.nextTick(() => {
// return result asynchronously
if (id === 42) {
callback(null, { name: 'Bob' })
} else {
callback(new Error('not found'))
}
})
},
}
function sayHi1(userId, callback) {
demoDatabase.findUser(userId, (err, user) => {
if (err) return callback(err)
callback(null, 'Hi ' + user.name)
})
}
sayHi1(42, (err, result) => {
if (err) {
console.error(err)
} else {
console.log(result)
}
})
sayHi1(43, (err, result) => {
if (err) {
console.error(err)
} else {
console.log(result)
}
})
function sayHi2(userId, callback) {
demoDatabase.findUser(userId, (err, user) => {
if (err) return callback(OError.tag(err))
callback(null, 'Hi ' + user.name)
})
}
sayHi2(43, (err, result) => {
if (err) {
console.error(OError.getFullStack(OError.tag(err)))
} else {
console.log(result)
}
})
function sayHi3(userId, callback) {
demoDatabase.findUser(userId, (err, user) => {
if (err) return callback(OError.tag(err, 'failed to find user', { userId }))
callback(null, 'Hi ' + user.name)
})
}
sayHi3(43, (err, result) => {
if (err) {
OError.tag(err, 'failed to say hi')
console.error(OError.getFullStack(err))
console.error(OError.getFullInfo(err))
} else {
console.log(result)
}
})
const promisify = require('util').promisify
demoDatabase.findUserAsync = promisify(demoDatabase.findUser)
async function sayHi4NoHandling(userId) {
const user = await demoDatabase.findUserAsync(userId)
return `Hi ${user.name}`
}
async function sayHi4(userId) {
try {
const user = await demoDatabase.findUserAsync(userId)
return `Hi ${user.name}`
} catch (error) {
throw OError.tag(error, 'failed to find user', { userId })
}
}
async function main() {
try {
await sayHi4NoHandling(43)
} catch (error) {
console.error(OError.getFullStack(error))
console.error(OError.getFullInfo(error))
}
try {
await sayHi4(43)
} catch (error) {
OError.tag(error, 'failed to say hi')
console.error(OError.getFullStack(error))
console.error(OError.getFullInfo(error))
}
}
main()
class UserNotFoundError extends OError {
constructor(userId) {
super('user not found', { userId })
}
}
try {
throw new UserNotFoundError(123)
} catch (error) {
console.error(OError.getFullStack(error))
console.error(OError.getFullInfo(error))
}
async function sayHi5(userId) {
try {
const user = await demoDatabase.findUserAsync(userId)
return `Hi ${user.name}`
} catch (error) {
if (error.message === 'not found') {
throw new UserNotFoundError(userId).withCause(error)
}
}
}
async function main2() {
try {
await sayHi5(43)
} catch (error) {
OError.tag(error, 'failed to say hi')
console.error(OError.getFullStack(error))
console.error(OError.getFullInfo(error))
}
}
main2()

View file

@ -0,0 +1,49 @@
#!/usr/bin/env node
const fs = require('fs')
const jsdoc2md = require('jsdoc-to-markdown')
const toc = require('markdown-toc')
const README = 'README.md'
const HEADER = '## OError API Reference'
const FOOTER = '<!-- END API REFERENCE -->'
async function main() {
const apiDocs = await jsdoc2md.render({ files: 'index.js' })
const apiDocLines = apiDocs.trim().split(/\r?\n/g)
// The first few lines don't make much sense when included in the README.
const apiDocStart = apiDocLines.indexOf('* [OError](#OError)')
if (apiDocStart === -1) {
console.error('API docs not in expected format for insertion.')
process.exit(1)
}
apiDocLines.splice(1, apiDocStart - 1)
apiDocLines.unshift(HEADER, '')
const readme = await fs.promises.readFile(README, { encoding: 'utf8' })
const readmeLines = readme.split(/\r?\n/g)
const apiStart = readmeLines.indexOf(HEADER)
const apiEnd = readmeLines.indexOf(FOOTER)
if (apiStart === -1 || apiEnd === -1) {
console.error('Could not find the API Reference section.')
process.exit(1)
}
Array.prototype.splice.apply(
readmeLines,
[apiStart, apiEnd - apiStart].concat(apiDocLines)
)
const readmeWithApi = readmeLines.join('\n')
let readmeWithApiAndToc = toc.insert(readmeWithApi)
// Unfortunately, the ⇒ breaks the generated TOC links.
readmeWithApiAndToc = readmeWithApiAndToc.replace(/-%E2%87%92-/g, '--')
await fs.promises.writeFile(README, readmeWithApiAndToc)
}
main()

190
libraries/o-error/index.js Normal file
View file

@ -0,0 +1,190 @@
/**
* Light-weight helpers for handling JavaScript Errors in node.js and the
* browser.
*/
class OError extends Error {
/**
* @param {string} message as for built-in Error
* @param {Object} [info] extra data to attach to the error
* @param {Error} [cause] the internal error that caused this error
*/
constructor(message, info, cause) {
super(message)
this.name = this.constructor.name
if (info) this.info = info
if (cause) this.cause = cause
/** @private @type {Array<TaggedError> | undefined} */
this._oErrorTags // eslint-disable-line
}
/**
* Set the extra info object for this error.
*
* @param {Object} info extra data to attach to the error
* @return {this}
*/
withInfo(info) {
this.info = info
return this
}
/**
* Wrap the given error, which caused this error.
*
* @param {Error} cause the internal error that caused this error
* @return {this}
*/
withCause(cause) {
this.cause = cause
return this
}
/**
* Tag debugging information onto any error (whether an OError or not) and
* return it.
*
* @example <caption>An error in a callback</caption>
* function findUser(name, callback) {
* fs.readFile('/etc/passwd', (err, data) => {
* if (err) return callback(OError.tag(err, 'failed to read passwd'))
* // ...
* })
* }
*
* @example <caption>A possible error in a callback</caption>
* function cleanup(callback) {
* fs.unlink('/tmp/scratch', (err) => callback(err && OError.tag(err)))
* }
*
* @example <caption>An error with async/await</caption>
* async function cleanup() {
* try {
* await fs.promises.unlink('/tmp/scratch')
* } catch (err) {
* throw OError.tag(err, 'failed to remove scratch file')
* }
* }
*
* @param {Error} error the error to tag
* @param {string} [message] message with which to tag `error`
* @param {Object} [info] extra data with wich to tag `error`
* @return {Error} the modified `error` argument
*/
static tag(error, message, info) {
const oError = /** @type{OError} */ (error)
if (!oError._oErrorTags) oError._oErrorTags = []
let tag
if (Error.captureStackTrace) {
// Hide this function in the stack trace, and avoid capturing it twice.
tag = /** @type TaggedError */ ({ name: 'TaggedError', message, info })
Error.captureStackTrace(tag, OError.tag)
} else {
tag = new TaggedError(message || '', info)
}
if (oError._oErrorTags.length >= OError.maxTags) {
// Preserve the first tag and add an indicator that we dropped some tags.
if (oError._oErrorTags[1] === DROPPED_TAGS_ERROR) {
oError._oErrorTags.splice(2, 1)
} else {
oError._oErrorTags[1] = DROPPED_TAGS_ERROR
}
}
oError._oErrorTags.push(tag)
return error
}
/**
* The merged info from any `tag`s and causes on the given error.
*
* If an info property is repeated, the last one wins.
*
* @param {Error | null | undefined} error any error (may or may not be an `OError`)
* @return {Object}
*/
static getFullInfo(error) {
const info = {}
if (!error) return info
const oError = /** @type{OError} */ (error)
if (oError.cause) Object.assign(info, OError.getFullInfo(oError.cause))
if (typeof oError.info === 'object') Object.assign(info, oError.info)
if (oError._oErrorTags) {
for (const tag of oError._oErrorTags) {
Object.assign(info, tag.info)
}
}
return info
}
/**
* Return the `stack` property from `error`, including the `stack`s for any
* tagged errors added with `OError.tag` and for any `cause`s.
*
* @param {Error | null | undefined} error any error (may or may not be an `OError`)
* @return {string}
*/
static getFullStack(error) {
if (!error) return ''
const oError = /** @type{OError} */ (error)
let stack = oError.stack || '(no stack)'
if (Array.isArray(oError._oErrorTags) && oError._oErrorTags.length) {
stack += `\n${oError._oErrorTags.map((tag) => tag.stack).join('\n')}`
}
const causeStack = oError.cause && OError.getFullStack(oError.cause)
if (causeStack) {
stack += '\ncaused by:\n' + indent(causeStack)
}
return stack
}
}
/**
* Maximum number of tags to apply to any one error instance. This is to avoid
* a resource leak in the (hopefully unlikely) case that a singleton error
* instance is returned to many callbacks. If tags have been dropped, the full
* stack trace will include a placeholder tag `... dropped tags`.
*
* Defaults to 100. Must be at least 1.
*
* @type {Number}
*/
OError.maxTags = 100
/**
* Used to record a stack trace every time we tag info onto an Error.
*
* @private
* @extends OError
*/
class TaggedError extends OError {}
const DROPPED_TAGS_ERROR = /** @type{TaggedError} */ ({
name: 'TaggedError',
message: '... dropped tags',
stack: 'TaggedError: ... dropped tags',
})
/**
* @private
* @param {string} string
* @return {string}
*/
function indent(string) {
return string.replace(/^/gm, ' ')
}
module.exports = OError

7253
libraries/o-error/package-lock.json generated Normal file

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,65 @@
{
"name": "@overleaf/o-error",
"version": "3.3.1",
"description": "Light-weight helpers for handling JavaScript Errors in node.js and the browser. Helps with long stack traces, Error subclasses, wrapping internal errors (causes), and attaching extra data to errors for logging.",
"keywords": [
"browser",
"node",
"error",
"long stack trace",
"stack trace",
"stack",
"cause",
"verror"
],
"main": "dist/index.js",
"files": [
"dist/index.js",
"dist/index.d.ts"
],
"types": "dist/index.d.ts",
"scripts": {
"clean": "rm -rf dist",
"build": "npm run --silent clean && npm run --silent typecheck && npm run --silent test && npm run --silent build:compile && npm run --silent declaration:build && npm run --silent update-readme",
"build:compile": "babel index.js --out-dir dist",
"declaration:build": "rm -f dist/index.d.ts && tsc --allowJs --declaration --emitDeclarationOnly --outDir dist --moduleResolution node --target ES6 index.js",
"declaration:check": "git diff --exit-code -- dist/index.d.ts",
"lint": "eslint .",
"prepublishOnly": "npm run --silent declaration:build && npm run --silent declaration:check",
"test": "mocha",
"test:coverage": "nyc --reporter=lcov --reporter=text-summary npm run test",
"typecheck": "tsc --allowJs --checkJs --noEmit --moduleResolution node --strict --target ES6 *.js test/**/*.js",
"update-readme": "doc/update-readme.js"
},
"author": "Overleaf (https://www.overleaf.com)",
"license": "MIT",
"repository": "github:overleaf/o-error",
"devDependencies": {
"@babel/cli": "^7.12.10",
"@babel/core": "^7.12.10",
"@babel/preset-env": "^7.12.11",
"@types/chai": "^4.2.12",
"@types/node": "^13.13.2",
"chai": "^3.3.0",
"eslint": "^6.8.0",
"eslint-config-prettier": "^6.10.1",
"eslint-config-standard": "^14.1.1",
"eslint-plugin-chai-expect": "^2.1.0",
"eslint-plugin-chai-friendly": "^0.5.0",
"eslint-plugin-import": "^2.20.2",
"eslint-plugin-mocha": "^6.3.0",
"eslint-plugin-node": "^11.1.0",
"eslint-plugin-prettier": "^3.1.3",
"eslint-plugin-promise": "^4.2.1",
"eslint-plugin-standard": "^4.0.1",
"jsdoc-to-markdown": "^5.0.3",
"markdown-toc": "^1.2.0",
"mocha": "^7.1.1",
"nyc": "^15.0.1",
"prettier": "^2.0.2",
"typescript": "^3.8.3"
},
"dependencies": {
"core-js": "^3.8.3"
}
}

View file

@ -0,0 +1,407 @@
const { expect } = require('chai')
const { promisify } = require('util')
const OError = require('..')
const {
expectError,
expectFullStackWithoutStackFramesToEqual,
} = require('./support')
describe('OError.tag', function () {
it('tags errors thrown from an async function', async function () {
const delay = promisify(setTimeout)
async function foo() {
await delay(10)
throw new Error('foo error')
}
async function bar() {
try {
await foo()
} catch (error) {
throw OError.tag(error, 'failed to bar', { bar: 'baz' })
}
}
async function baz() {
try {
await bar()
} catch (error) {
throw OError.tag(error, 'failed to baz', { baz: 'bat' })
}
}
try {
await baz()
expect.fail('should have thrown')
} catch (error) {
expectError(error, {
name: 'Error',
klass: Error,
message: 'Error: foo error',
firstFrameRx: /at foo/,
})
expectFullStackWithoutStackFramesToEqual(error, [
'Error: foo error',
'TaggedError: failed to bar',
'TaggedError: failed to baz',
])
expect(OError.getFullInfo(error)).to.eql({
bar: 'baz',
baz: 'bat',
})
}
})
it('tags errors thrown from a promise rejection', async function () {
function foo() {
return new Promise((resolve, reject) => {
setTimeout(function () {
reject(new Error('foo error'))
}, 10)
})
}
async function bar() {
try {
await foo()
} catch (error) {
throw OError.tag(error, 'failed to bar', { bar: 'baz' })
}
}
async function baz() {
try {
await bar()
} catch (error) {
throw OError.tag(error, 'failed to baz', { baz: 'bat' })
}
}
try {
await baz()
expect.fail('should have thrown')
} catch (error) {
expectError(error, {
name: 'Error',
klass: Error,
message: 'Error: foo error',
firstFrameRx: /_onTimeout/,
})
expectFullStackWithoutStackFramesToEqual(error, [
'Error: foo error',
'TaggedError: failed to bar',
'TaggedError: failed to baz',
])
expect(OError.getFullInfo(error)).to.eql({
bar: 'baz',
baz: 'bat',
})
}
})
it('tags errors yielded through callbacks', function (done) {
function foo(cb) {
setTimeout(function () {
cb(new Error('foo error'))
}, 10)
}
function bar(cb) {
foo(function (err) {
if (err) {
return cb(OError.tag(err, 'failed to bar', { bar: 'baz' }))
}
cb()
})
}
function baz(cb) {
bar(function (err) {
if (err) {
return cb(OError.tag(err, 'failed to baz', { baz: 'bat' }))
}
cb()
})
}
baz(function (err) {
if (err) {
expectError(err, {
name: 'Error',
klass: Error,
message: 'Error: foo error',
firstFrameRx: /_onTimeout/,
})
expectFullStackWithoutStackFramesToEqual(err, [
'Error: foo error',
'TaggedError: failed to bar',
'TaggedError: failed to baz',
])
expect(OError.getFullInfo(err)).to.eql({
bar: 'baz',
baz: 'bat',
})
return done()
}
expect.fail('should have yielded an error')
})
})
it('is not included in the stack trace if using capture', function () {
if (!Error.captureStackTrace) return this.skip()
const err = new Error('test error')
OError.tag(err, 'test message')
const stack = OError.getFullStack(err)
expect(stack).to.match(/TaggedError: test message\n\s+at/)
expect(stack).to.not.match(/TaggedError: test message\n\s+at [\w.]*tag/)
})
describe('without Error.captureStackTrace', function () {
/* eslint-disable mocha/no-hooks-for-single-case */
before(function () {
this.originalCaptureStackTrace = Error.captureStackTrace
Error.captureStackTrace = null
})
after(function () {
Error.captureStackTrace = this.originalCaptureStackTrace
})
it('still captures a stack trace, albeit including itself', function () {
const err = new Error('test error')
OError.tag(err, 'test message')
expectFullStackWithoutStackFramesToEqual(err, [
'Error: test error',
'TaggedError: test message',
])
const stack = OError.getFullStack(err)
expect(stack).to.match(/TaggedError: test message\n\s+at [\w.]*tag/)
})
})
describe('with limit on the number of tags', function () {
before(function () {
this.originalMaxTags = OError.maxTags
OError.maxTags = 3
})
after(function () {
OError.maxTags = this.originalMaxTags
})
it('should not tag more than that', function () {
const err = new Error('test error')
OError.tag(err, 'test message 1')
OError.tag(err, 'test message 2')
OError.tag(err, 'test message 3')
OError.tag(err, 'test message 4')
OError.tag(err, 'test message 5')
expectFullStackWithoutStackFramesToEqual(err, [
'Error: test error',
'TaggedError: test message 1',
'TaggedError: ... dropped tags',
'TaggedError: test message 4',
'TaggedError: test message 5',
])
})
it('should handle deep recursion', async function () {
async function recursiveAdd(n) {
try {
if (n === 0) throw new Error('deep error')
const result = await recursiveAdd(n - 1)
return result + 1
} catch (err) {
throw OError.tag(err, `at level ${n}`)
}
}
try {
await recursiveAdd(10)
} catch (err) {
expectFullStackWithoutStackFramesToEqual(err, [
'Error: deep error',
'TaggedError: at level 0',
'TaggedError: ... dropped tags',
'TaggedError: at level 9',
'TaggedError: at level 10',
])
}
})
it('should handle a singleton error', function (done) {
const err = new Error('singleton error')
function endpoint(callback) {
helper((err) => callback(err && OError.tag(err, 'in endpoint')))
}
function helper(callback) {
libraryFunction((err) => callback(err && OError.tag(err, 'in helper')))
}
function libraryFunction(callback) {
callback(err)
}
endpoint(() => {
endpoint((err) => {
expect(err).to.exist
expectFullStackWithoutStackFramesToEqual(err, [
'Error: singleton error',
'TaggedError: in helper',
'TaggedError: ... dropped tags',
'TaggedError: in helper',
'TaggedError: in endpoint',
])
done()
})
})
})
})
})
describe('OError.getFullInfo', function () {
it('works when given null', function () {
expect(OError.getFullInfo(null)).to.deep.equal({})
})
it('works on a normal error', function () {
const err = new Error('foo')
expect(OError.getFullInfo(err)).to.deep.equal({})
})
it('works on an error with tags', function () {
const err = OError.tag(new Error('foo'), 'bar', { userId: 123 })
expect(OError.getFullInfo(err)).to.deep.equal({ userId: 123 })
})
it('merges info from an error and its tags', function () {
const err = new OError('foo').withInfo({ projectId: 456 })
OError.tag(err, 'failed to foo', { userId: 123 })
expect(OError.getFullInfo(err)).to.deep.equal({
projectId: 456,
userId: 123,
})
})
it('merges info from a cause', function () {
const err1 = new Error('foo')
const err2 = new Error('bar')
err1.cause = err2
err2.info = { userId: 123 }
expect(OError.getFullInfo(err1)).to.deep.equal({ userId: 123 })
})
it('merges info from a nested cause', function () {
const err1 = new Error('foo')
const err2 = new Error('bar')
const err3 = new Error('baz')
err1.cause = err2
err2.info = { userId: 123 }
err2.cause = err3
err3.info = { foo: 42 }
expect(OError.getFullInfo(err1)).to.deep.equal({
userId: 123,
foo: 42,
})
})
it('merges info from cause with duplicate keys', function () {
const err1 = new Error('foo')
const err2 = new Error('bar')
err1.info = { userId: 42, foo: 1337 }
err1.cause = err2
err2.info = { userId: 1 }
expect(OError.getFullInfo(err1)).to.deep.equal({
userId: 42,
foo: 1337,
})
})
it('merges info from tags with duplicate keys', function () {
const err1 = OError.tag(new Error('foo'), 'bar', { userId: 123 })
const err2 = OError.tag(err1, 'bat', { userId: 456 })
expect(OError.getFullInfo(err2)).to.deep.equal({ userId: 456 })
})
it('works on an error with .info set to a string', function () {
const err = new Error('foo')
err.info = 'test'
expect(OError.getFullInfo(err)).to.deep.equal({})
})
})
describe('OError.getFullStack', function () {
it('works when given null', function () {
expect(OError.getFullStack(null)).to.equal('')
})
it('works on a normal error', function () {
const err = new Error('foo')
const fullStack = OError.getFullStack(err)
expect(fullStack).to.match(/^Error: foo$/m)
expect(fullStack).to.match(/^\s+at /m)
})
it('works on an error with a cause', function () {
const err1 = new Error('foo')
const err2 = new Error('bar')
err1.cause = err2
const fullStack = OError.getFullStack(err1)
expect(fullStack).to.match(/^Error: foo$/m)
expect(fullStack).to.match(/^\s+at /m)
expect(fullStack).to.match(/^caused by:\n\s+Error: bar$/m)
})
it('works on both tags and causes', async function () {
// Here's the actual error.
function tryToFoo() {
try {
throw Error('foo')
} catch (error) {
throw OError.tag(error, 'failed to foo', { foo: 1 })
}
}
// Inside another function that wraps it.
function tryToBar() {
try {
tryToFoo()
} catch (error) {
throw new OError('failed to bar').withCause(error)
}
}
// And it is in another try.
try {
try {
tryToBar()
expect.fail('should have thrown')
} catch (error) {
throw OError.tag(error, 'failed to bat', { bat: 1 })
}
} catch (error) {
// We catch the wrapping error.
expectError(error, {
name: 'OError',
klass: OError,
message: 'OError: failed to bar',
firstFrameRx: /tryToBar/,
})
// But the stack contains all of the errors and tags.
expectFullStackWithoutStackFramesToEqual(error, [
'OError: failed to bar',
'TaggedError: failed to bat',
'caused by:',
' Error: foo',
' TaggedError: failed to foo',
])
// The info from the wrapped cause should be picked up for logging.
expect(OError.getFullInfo(error)).to.eql({ bat: 1, foo: 1 })
// But it should still be recorded.
expect(OError.getFullInfo(error.cause)).to.eql({ foo: 1 })
}
})
})

View file

@ -0,0 +1,111 @@
const { expect } = require('chai')
const OError = require('..')
const {
expectError,
expectFullStackWithoutStackFramesToEqual,
} = require('./support')
class CustomError1 extends OError {
constructor() {
super('failed to foo')
}
}
class CustomError2 extends OError {
constructor(customMessage) {
super(customMessage || 'failed to bar')
}
}
describe('OError', function () {
it('can have an info object', function () {
const err1 = new OError('foo', { foo: 1 })
expect(err1.info).to.eql({ foo: 1 })
const err2 = new OError('foo').withInfo({ foo: 2 })
expect(err2.info).to.eql({ foo: 2 })
})
it('can have a cause', function () {
const err1 = new OError('foo', { foo: 1 }, new Error('cause 1'))
expect(err1.cause.message).to.equal('cause 1')
const err2 = new OError('foo').withCause(new Error('cause 2'))
expect(err2.cause.message).to.equal('cause 2')
})
it('handles a custom error type with a cause', function () {
function doSomethingBadInternally() {
throw new Error('internal error')
}
function doSomethingBad() {
try {
doSomethingBadInternally()
} catch (error) {
throw new CustomError1().withCause(error)
}
}
try {
doSomethingBad()
expect.fail('should have thrown')
} catch (error) {
expectError(error, {
name: 'CustomError1',
klass: CustomError1,
message: 'CustomError1: failed to foo',
firstFrameRx: /doSomethingBad/,
})
expect(OError.getFullInfo(error)).to.deep.equal({})
expectFullStackWithoutStackFramesToEqual(error, [
'CustomError1: failed to foo',
'caused by:',
' Error: internal error',
])
}
})
it('handles a custom error type with nested causes', function () {
function doSomethingBadInternally() {
throw new Error('internal error')
}
function doBar() {
try {
doSomethingBadInternally()
} catch (error) {
throw new CustomError2('failed to bar!').withCause(error)
}
}
function doFoo() {
try {
doBar()
} catch (error) {
throw new CustomError1().withCause(error)
}
}
try {
doFoo()
expect.fail('should have thrown')
} catch (error) {
expectError(error, {
name: 'CustomError1',
klass: CustomError1,
message: 'CustomError1: failed to foo',
firstFrameRx: /doFoo/,
})
expectFullStackWithoutStackFramesToEqual(error, [
'CustomError1: failed to foo',
'caused by:',
' CustomError2: failed to bar!',
' caused by:',
' Error: internal error',
])
expect(OError.getFullInfo(error)).to.deep.equal({})
}
})
})

View file

@ -0,0 +1,61 @@
const { expect } = require('chai')
const OError = require('../..')
/**
* @param {Error} e
* @param {any} expected
*/
exports.expectError = function OErrorExpectError(e, expected) {
expect(
e.name,
"error should set the name property to the error's name"
).to.equal(expected.name)
expect(
e instanceof expected.klass,
'error should be an instance of the error type'
).to.be.true
expect(
e instanceof Error,
'error should be an instance of the built-in Error type'
).to.be.true
expect(
require('util').types.isNativeError(e),
'error should be recognised by util.types.isNativeError'
).to.be.true
expect(
e.toString(),
'toString should return the default error message formatting'
).to.equal(expected.message)
expect(e.stack, 'error should have a stack trace').to.not.be.empty
expect(
/** @type {string} */ (e.stack).split('\n')[0],
'stack should start with the default error message formatting'
).to.match(new RegExp(`^${expected.name}:`))
expect(
/** @type {string} */ (e.stack).split('\n')[1],
'first stack frame should be the function where the error was thrown'
).to.match(expected.firstFrameRx)
}
/**
* @param {Error} error
* @param {String[]} expected
*/
exports.expectFullStackWithoutStackFramesToEqual = function (error, expected) {
const fullStack = OError.getFullStack(error)
const fullStackWithoutFrames = fullStack
.split('\n')
.filter((line) => !/^\s+at\s/.test(line))
expect(
fullStackWithoutFrames,
'full stack without frames should equal'
).to.deep.equal(expected)
}