Skip to content

Commit 0ccfa26

Browse files
authored
feat(serializers): Implemented "error with cause" serializer (#130)
1 parent b4edb5d commit 0ccfa26

File tree

9 files changed

+389
-41
lines changed

9 files changed

+389
-41
lines changed

Readme.md

Lines changed: 74 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,12 +16,84 @@ Serializes an `Error` like object. Returns an object:
1616
raw: Error // Non-enumerable, i.e. will not be in the output, original
1717
// Error object. This is available for subsequent serializers
1818
// to use.
19+
[...any additional Enumerable property the original Error had]
1920
}
2021
```
2122

2223
Any other extra properties, e.g. `statusCode`, that have been attached to the
2324
object will also be present on the serialized object.
2425

26+
If the error object has a [`cause`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Error/cause) property, the `cause`'s `message` and `stack` will be appended to the top-level `message` and `stack`. All other parameters that belong to the `error.cause` object will be omitted.
27+
28+
Example:
29+
30+
```js
31+
const serializer = require('pino-std-serializers').err;
32+
33+
const innerError = new Error("inner error");
34+
innerError.isInner = true;
35+
const outerError = new Error("outer error", { cause: innerError });
36+
outerError.isInner = false;
37+
38+
const serialized = serializer(outerError);
39+
/* Result:
40+
{
41+
"type": "Error",
42+
"message": "outer error: inner error",
43+
"isInner": false,
44+
"stack": "Error: outer error
45+
at <...omitted..>
46+
caused by: Error: inner error
47+
at <...omitted..>
48+
}
49+
*/
50+
51+
### `exports.errWithCause(error)`
52+
Serializes an `Error` like object, including any `error.cause`. Returns an object:
53+
54+
```js
55+
{
56+
type: 'string', // The name of the object's constructor.
57+
message: 'string', // The supplied error message.
58+
stack: 'string', // The stack when the error was generated.
59+
cause?: Error, // If the original error had an error.cause, it will be serialized here
60+
raw: Error // Non-enumerable, i.e. will not be in the output, original
61+
// Error object. This is available for subsequent serializers
62+
// to use.
63+
[...any additional Enumerable property the original Error had]
64+
}
65+
```
66+
67+
Any other extra properties, e.g. `statusCode`, that have been attached to the object will also be present on the serialized object.
68+
69+
Example:
70+
```javascript
71+
const serializer = require('pino-std-serializers').errWithCause;
72+
73+
const innerError = new Error("inner error");
74+
innerError.isInner = true;
75+
const outerError = new Error("outer error", { cause: innerError });
76+
outerError.isInner = false;
77+
78+
const serialized = serializer(outerError);
79+
/* Result:
80+
{
81+
"type": "Error",
82+
"message": "outer error",
83+
"isInner": false,
84+
"stack": "Error: outer error
85+
at <...omitted..>",
86+
"cause": {
87+
"type": "Error",
88+
"message": "inner error",
89+
"isInner": true,
90+
"stack": "Error: inner error
91+
at <...omitted..>"
92+
},
93+
}
94+
*/
95+
```
96+
2597
### `exports.mapHttpResponse(response)`
2698
Used internally by Pino for general response logging. Returns an object:
2799

@@ -49,7 +121,7 @@ The default `request` serializer. Returns an object:
49121

50122
```js
51123
{
52-
id: 'string', // Defaults to `undefined`, unless there is an `id` property
124+
id: 'string', // Defaults to `undefined`, unless there is an `id` property
53125
// already attached to the `request` object or to the `request.info`
54126
// object. Attach a synchronous function
55127
// to the `request.id` that returns an identifier to have
@@ -64,7 +136,7 @@ The default `request` serializer. Returns an object:
64136
remotePort: Number,
65137
raw: Object // Non-enumerable, i.e. will not be in the output, original
66138
// request object. This is available for subsequent serializers
67-
// to use. In cases where the `request` input already has
139+
// to use. In cases where the `request` input already has
68140
// a `raw` property this will replace the original `request.raw`
69141
// property
70142
}

index.d.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,10 +32,16 @@ export interface SerializedError {
3232
}
3333

3434
/**
35-
* Serializes an Error object.
35+
* Serializes an Error object. Does not serialize "err.cause" fields (will append the err.cause.message to err.message
36+
* and err.cause.stack to err.stack)
3637
*/
3738
export function err(err: Error): SerializedError;
3839

40+
/**
41+
* Serializes an Error object, including full serialization for any err.cause fields recursively.
42+
*/
43+
export function errWithCause(err: Error): SerializedError;
44+
3945
export interface SerializedRequest {
4046
/**
4147
* Defaults to `undefined`, unless there is an `id` property already attached to the `request` object or

index.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,13 @@
11
'use strict'
22

33
const errSerializer = require('./lib/err')
4+
const errWithCauseSerializer = require('./lib/err-with-cause')
45
const reqSerializers = require('./lib/req')
56
const resSerializers = require('./lib/res')
67

78
module.exports = {
89
err: errSerializer,
10+
errWithCause: errWithCauseSerializer,
911
mapHttpRequest: reqSerializers.mapHttpRequest,
1012
mapHttpResponse: resSerializers.mapHttpResponse,
1113
req: reqSerializers.reqSerializer,

lib/err-proto.js

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
'use strict'
2+
3+
const seen = Symbol('circular-ref-tag')
4+
const rawSymbol = Symbol('pino-raw-err-ref')
5+
6+
const pinoErrProto = Object.create({}, {
7+
type: {
8+
enumerable: true,
9+
writable: true,
10+
value: undefined
11+
},
12+
message: {
13+
enumerable: true,
14+
writable: true,
15+
value: undefined
16+
},
17+
stack: {
18+
enumerable: true,
19+
writable: true,
20+
value: undefined
21+
},
22+
aggregateErrors: {
23+
enumerable: true,
24+
writable: true,
25+
value: undefined
26+
},
27+
raw: {
28+
enumerable: false,
29+
get: function () {
30+
return this[rawSymbol]
31+
},
32+
set: function (val) {
33+
this[rawSymbol] = val
34+
}
35+
}
36+
})
37+
Object.defineProperty(pinoErrProto, rawSymbol, {
38+
writable: true,
39+
value: {}
40+
})
41+
42+
module.exports = {
43+
pinoErrProto,
44+
pinoErrorSymbols: {
45+
seen,
46+
rawSymbol
47+
}
48+
}

lib/err-with-cause.js

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
'use strict'
2+
3+
module.exports = errWithCauseSerializer
4+
5+
const { isErrorLike } = require('./err-helpers')
6+
const { pinoErrProto, pinoErrorSymbols } = require('./err-proto')
7+
const { seen } = pinoErrorSymbols
8+
9+
const { toString } = Object.prototype
10+
11+
function errWithCauseSerializer (err) {
12+
if (!isErrorLike(err)) {
13+
return err
14+
}
15+
16+
err[seen] = undefined // tag to prevent re-looking at this
17+
const _err = Object.create(pinoErrProto)
18+
_err.type = toString.call(err.constructor) === '[object Function]'
19+
? err.constructor.name
20+
: err.name
21+
_err.message = err.message
22+
_err.stack = err.stack
23+
24+
if (Array.isArray(err.errors)) {
25+
_err.aggregateErrors = err.errors.map(err => errWithCauseSerializer(err))
26+
}
27+
28+
if (isErrorLike(err.cause) && !Object.prototype.hasOwnProperty.call(err.cause, seen)) {
29+
_err.cause = errWithCauseSerializer(err.cause)
30+
}
31+
32+
for (const key in err) {
33+
if (_err[key] === undefined) {
34+
const val = err[key]
35+
if (isErrorLike(val)) {
36+
if (!Object.prototype.hasOwnProperty.call(val, seen)) {
37+
_err[key] = errWithCauseSerializer(val)
38+
}
39+
} else {
40+
_err[key] = val
41+
}
42+
}
43+
}
44+
45+
delete err[seen] // clean up tag in case err is serialized again later
46+
_err.raw = err
47+
return _err
48+
}

lib/err.js

Lines changed: 2 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -3,45 +3,10 @@
33
module.exports = errSerializer
44

55
const { messageWithCauses, stackWithCauses, isErrorLike } = require('./err-helpers')
6+
const { pinoErrProto, pinoErrorSymbols } = require('./err-proto')
7+
const { seen } = pinoErrorSymbols
68

79
const { toString } = Object.prototype
8-
const seen = Symbol('circular-ref-tag')
9-
const rawSymbol = Symbol('pino-raw-err-ref')
10-
const pinoErrProto = Object.create({}, {
11-
type: {
12-
enumerable: true,
13-
writable: true,
14-
value: undefined
15-
},
16-
message: {
17-
enumerable: true,
18-
writable: true,
19-
value: undefined
20-
},
21-
stack: {
22-
enumerable: true,
23-
writable: true,
24-
value: undefined
25-
},
26-
aggregateErrors: {
27-
enumerable: true,
28-
writable: true,
29-
value: undefined
30-
},
31-
raw: {
32-
enumerable: false,
33-
get: function () {
34-
return this[rawSymbol]
35-
},
36-
set: function (val) {
37-
this[rawSymbol] = val
38-
}
39-
}
40-
})
41-
Object.defineProperty(pinoErrProto, rawSymbol, {
42-
writable: true,
43-
value: {}
44-
})
4510

4611
function errSerializer (err) {
4712
if (!isErrorLike(err)) {

0 commit comments

Comments
 (0)