Repository: pinojs/pino (js)

Issue:
Invalid error JSON logged if stack is undefined
To reproduce:

```js
const logger = require('pino')();
const err = new Error();
delete err.stack;
logger.error(err);

/*
{"level":50,"time":1532087320790,"msg":"","pid":20631,"hostname":"dev.local","type":"Error","stack":undefined,"v":1}
*/
```
Note the section `"stack":undefined` which is not valid JSON.

Real world example: Apollo GraphQL has a custom error class called ApolloError that does not hold `stack` property. When logging an ApolloError we see the issue above which breaks pino's pretty printer among other things.

Pino version: 4.17.3

Pull request:
fix: support logging errors that do not contain a stack
Fixes #456 

Custom errors may not have their stack property set.
Pino used to serialize undefined stack which created invalid JSON output

Given the following contrived example:
```js
const logger = require('pino')();
const err = new Error();
delete err.stack;
logger.error(err);
```
Would output:
```
{"level":50,"time":1532087320790,"msg":"","pid":20631,"hostname":"dev.local","type":"Error","stack":undefined,"v":1}
```
This is invalid JSON due to the `"stack":undefined` at the end of that output line.

This PR adds a fix to avoid logging the stack if it is not set. So the above code now outputs:

```
{"level":50,"time":1532094347667,"msg":"","pid":25624,"hostname":"dev.local","type":"Error","v":1}
```

Patch:
diff --git a/pino.js b/pino.js
index d7ad5b7..ff34c3b 100644
--- a/pino.js
+++ b/pino.js
@@ -146,7 +146,8 @@ function asJson (obj, msg, num, time) {
   if (hasObj) {
     var notHasOwnProperty = obj.hasOwnProperty === undefined
     if (objError) {
-      data += ',"type":"Error","stack":' + this.stringify(obj.stack)
+      data += ',"type":"Error"'
+      data += obj.stack ? ',"stack":' + this.stringify(obj.stack) : ''
     }
     // if global serializer is set, call it first
     if (this.serializers[Symbol.for('pino.*')]) {
diff --git a/pretty.js b/pretty.js
index 6d8c2d8..a2bdeeb 100755
--- a/pretty.js
+++ b/pretty.js
@@ -217,7 +217,9 @@ function pretty (opts) {
     line += eol
 
     if (value.type === 'Error') {
-      line += '    ' + withSpaces(value.stack, eol) + eol
+      line += value.stack
+        ? '    ' + withSpaces(value.stack, eol) + eol
+        : ''
 
       var propsForPrint
       if (errorProps && errorProps.length > 0) {


Test patch:
diff --git a/test/error.test.js b/test/error.test.js
index 005017c..dbdf1bc 100644
--- a/test/error.test.js
+++ b/test/error.test.js
@@ -128,3 +128,18 @@ test('an error with statusCode property is not confused for a http response', fu
   instance.level = name
   instance[name](err)
 })
+
+test('stack is omitted if it is not set on err', t => {
+  t.plan(2)
+  var err = new Error('myerror')
+  delete err.stack
+  var instance = pino(sink(function (chunk, enc, cb) {
+    t.ok(new Date(chunk.time) <= new Date(), 'time is greater than Date.now()')
+    delete chunk.time
+    t.equal(chunk.hasOwnProperty('stack'), false)
+    cb()
+  }))
+
+  instance.level = name
+  instance[name](err)
+})
diff --git a/test/pretty.test.js b/test/pretty.test.js
index 2933161..dc34ee2 100644
--- a/test/pretty.test.js
+++ b/test/pretty.test.js
@@ -105,6 +105,24 @@ test('pino transform prettifies Error', function (t) {
   instance.info(err)
 })
 
+test('pino transform prettifies Error without stack', function (t) {
+  var prettier = pretty()
+  var err = new Error('hello world')
+  delete err.stack
+  var expected = [err.message]
+
+  t.plan(expected.length)
+
+  prettier.pipe(split(function (line) {
+    t.ok(line.indexOf(expected.shift()) >= 0, 'line matches')
+    return line
+  }))
+
+  var instance = pino(prettier)
+
+  instance.info(err)
+})
+
 function getIndentLevel (str) {
   return (/^\s*/.exec(str) || [''])[0].length
 }