Reporting JavaScript Error Details from Promises

October 27th 2017 JavaScript

Promises make it easy to handle errors in asynchronous method calls:

callApi().then(function(result) {
  // process promise result
}).catch(function(error) => {
  console.log('API error: ' + JSON.stringify(error));
});

If there's any error in the above API call, the rejection handler function in catch method will log it to console, including all the details for easier troubleshooting later:

API error: {"details":"error"}

Of course, in production code you'll want to post the error details to a remote error reporting service (e.g. Google Analytics) since you don't have access to your users' browser console.

Not only will the rejection handler catch rejected promises, it will also catch any errors that might happen in the fulfillment handler passed to the then method:

this.callApi().then(function(result) {
  var value = result.inner.value; // will throw error 
}).catch(function(error) => {
  console.log('API error: ' + JSON.stringify(error));
});

Surprisingly enough, the log won't include any useful details about the error in this case:

API error: {"__zone_symbol__currentTask":{"type":"microTask","state":"notScheduled","source":"Promise.then","zone":"angular","cancelFn":null,"runCount":0}}

Only a bunch of promise internals were logged, which are completely useless for determining the actual error. This is caused by the fact that the Error object properties in JavaScript are non-enumerable and therefore not serialized by JSON.stringify().

To work around that issue, a replacer function can be passed to JSON.stringify:

callApi().then(function(result) {
  var value = result.inner.value; // will throw error 
}).catch(function(error) => {
  console.log('API error: ' + JSON.stringify(error, replaceErrors));
});

The replacer function can implement a modified serialization process for the Error objects:

replaceErrors(key, value) {
  if (value instanceof Error) {
    var error = {};

    Object.getOwnPropertyNames(value).forEach(function(propertyName) {
      error[propertyName] = value[propertyName];
    });

    return error;
  }
  return value;
}

With this change in place, the serialized error will include all the information that's available and make troubleshooting possible:

API error: {"fileName":"http://localhost:8100/build/main.js","lineNumber":62,"columnNumber":17,"message":"result.inner is undefined","__zone_symbol__currentTask":{"type":"microTask","state":"notScheduled","source":"Promise.then","zone":"angular","cancelFn":null,"runCount":0}}

Since it makes sense to use the replacer function whenever you are serializing errors, you'll probably want to wrap the error reporting call along with the modified serialization in a common error reporting function to call throughout your application.

Copyright
Creative Commons License