93

How can I rethrow an error or exception in nodejs/javascript and include a custom message.

I have the following code

var json = JSON.parse(result);

and I wanted to include the result content in the exception message should any parsing error occurs. Something like this.

1.  try {
2.    var json = JSON.parse(result);
3.    expect(json.messages.length).to.be(1);
4.  } catch(ex) {
5.    throw new Error(ex.message + ". " + "JSON response: " + result);
6.  }

The problem here is that I lose my stack trace.

Is there a way to do it similar to java?

throw new Error("JSON response: " + result, ex);
2
  • 1
    Not a direct answer, but you really want to study this: joyent.com/node-js/production/design/errors
    – Paul
    Commented Mar 13, 2017 at 1:50
  • @Paul Although a great intro, I think that's a bit dated now as it has no reference to promises and error classes which solve a lot of the vagaries of JS/Node.js error handling.
    – Matt
    Commented Mar 13, 2017 at 3:00

6 Answers 6

106

ES2022 added the .cause non enumerable property to Error objects which can be set from the constructor

function doThing(){
  try {
    throw new Error('Internal Error')
  }
  catch (err) {
    throw new Error('Do thing failed!', { cause: err })
  }
}
try {
  doThing()
}
catch (err) {
  console.log('Error: ', err)
  console.log('Cause: ', err.cause)
}

Supported in:

Chrome  > 93
Firefox > 91
Safari  > 15
Node.js > v16.9.0

From the tc39 proposal, there is an es-shim Error implementation and how the cause property is set.

See Scotty Jamison's answer for more stack trace detail




Answer prior to 2022

I'm not aware of a native method like Java's and I've not found an elegant solution for wrapping errors yet.

The problem with creating a new Error is you can lose metadata that was attached to the original Error that was thrown, the stack trace and type are generally the important items lost.

Making modifications to an existing thrown error is quicker, but it is still possible to modify data from the error out of existence. It also feels wrong to be poking around in an error that was created somewhere else.


Create a new Error and new stack

The .stack property of a new Error is a plain string and can be modified to say what you like before it is thrown. Replacing an errors stack property completely can get really confusing for debugging though.

When the original thrown error and the error handler are in separate locations or files (which is common with promises), you might be able to trace the source of the original error but not trace the handler where the error was actually trapped. To avoid this it's good to keep some references to both the original and new error in the stack. It's also useful to have access to the complete original error if there was additional metadata stored in it.

Here's an example of catching an error, wrapping it in a new error but adding the original stack and storing the error:

try {
  throw new Error('First one')
} catch (error) {
  let e = new Error(`Rethrowing the "${error.message}" error`)
  e.original_error = error
  e.stack = e.stack.split('\n').slice(0,2).join('\n') + '\n' +
            error.stack
  throw e
}

Which throws:

/so/42754270/test.js:9
    throw e
    ^

Error: Rethrowing the "First one" error
    at test (/so/42754270/test.js:5:13)
Error: First one
    at test (/so/42754270/test.js:3:11)
    at Object.<anonymous> (/so/42754270/test.js:13:1)
    at Module._compile (module.js:570:32)
    at Object.Module._extensions..js (module.js:579:10)
    at Module.load (module.js:487:32)
    at tryModuleLoad (module.js:446:12)
    at Function.Module._load (module.js:438:3)
    at Module.runMain (module.js:604:10)
    at run (bootstrap_node.js:394:7)
    at startup (bootstrap_node.js:149:9)

So we've created a new generic Error. Unfortunately the type of the original error becomes hidden from the output but the error has been attached as .original_error so it can still be accessed. The new stack has been largely removed except for the generating line which is important, and the original errors stack appended.

Any tools that try to parse stack traces might not work with this change or best case, they detect two errors.

Rethrowing with ES2015+ Error Classes

Making this into a reusable ES2015+ Error class:

class RethrownError extends Error {
  constructor(message, error){
    super(message)
    this.name = this.constructor.name
    if (!error) throw new Error('RethrownError requires a message and error')
    this.original_error = error
    this.stack_before_rethrow = this.stack
    const message_lines =  (this.message.match(/\n/g)||[]).length + 1
    this.stack = this.stack.split('\n').slice(0, message_lines+1).join('\n') + '\n' +
                 error.stack
  }
}

throw new RethrownError(`Oh no a "${error.message}" error`, error)

Results in

/so/42754270/test2.js:31
    throw new RethrownError(`Oh no a "${error.message}"" error`, error)
    ^

RethrownError: Oh no a "First one" error
    at test (/so/42754270/test2.js:31:11)
Error: First one
    at test (/so/42754270/test2.js:29:11)
    at Object.<anonymous> (/so/42754270/test2.js:35:1)
    at Module._compile (module.js:570:32)
    at Object.Module._extensions..js (module.js:579:10)
    at Module.load (module.js:487:32)
    at tryModuleLoad (module.js:446:12)
    at Function.Module._load (module.js:438:3)
    at Module.runMain (module.js:604:10)
    at run (bootstrap_node.js:394:7)
    at startup (bootstrap_node.js:149:9)

Then you know that whenever you see a RethrownError that the original error will still be available at .original_error.

This method is not perfect but it means I can re-type known errors from underlying modules into generic types that be handled more easily, usually with bluebirds filtered catch .catch(TypeError, handler)

Note stack becomes enumerable here

Same Error with a modified stack

Some times you will need to keep the original error mostly as is.

In this case you can just append/insert the new info onto existing stack.

file = '/home/jim/plumbers'
try {
   JSON.parse('k')
} catch (e) {
   let message = `JSON parse error in ${file}`
   let stack = new Error(message).stack
   e.stack = e.stack + '\nFrom previous ' + stack.split('\n').slice(0,2).join('\n') + '\n'
   throw e
}

Which returns

/so/42754270/throw_error_replace_stack.js:13
       throw e
       ^

SyntaxError: Unexpected token k in JSON at position 0
    at Object.parse (native)
    at Object.<anonymous> (/so/42754270/throw_error_replace_stack.js:8:13)
    at Module._compile (module.js:570:32)
    at Object.Module._extensions..js (module.js:579:10)
    at Module.load (module.js:487:32)
    at tryModuleLoad (module.js:446:12)
    at Function.Module._load (module.js:438:3)
    at Module.runMain (module.js:604:10)
    at run (bootstrap_node.js:394:7)
    at startup (bootstrap_node.js:149:9)
From previous Error: JSON parse error in "/home/jim/plumbers"
    at Object.<anonymous> (/so/42754270/throw_error_replace_stack.js:11:20)

Also note that the stack processing is simple and assumes the error message is a single line. If you run into multi line error messages you might need to look for \n at to terminate the message.

3
  • Why is captureStackTrace required? I didn't find any scenario where it would make a difference. Commented Dec 16, 2020 at 16:01
  • hmmm, probably not relevant any more. I haven't changed the base much since it was initially written early ES6. If memory serves it was an ES5 transpile thing, to remove the custom Error code from the top of the stack in v8 browsers. There's a bit of related discussion here.
    – Matt
    Commented Dec 17, 2020 at 1:44
  • This was an excellent solution at its time. but since 2021 there is a v8-native mechanism -- see @scotty-jamison's answer below: stackoverflow.com/a/73545352/178551
    – jsalvata
    Commented Mar 14, 2023 at 2:22
26

If all you want to do is change the message, you can just change the message:

try {
  throw new Error("Original Error");
} catch(err) {
  err.message = "Here is some context -- " + err.message;
  throw err;
}

UPDATE:

If the message property is readonly, you can create a new object using the original error as the prototype and assigning a new message:

try {  // line 12
  document.querySelectorAll("div:foo");  // Throws a DOMException (invalid selector)
} catch(err) {
  let message = "Here is some context -- " + err.message;
  let e = Object.create( err, { message: { value: message } } );
  throw e;  // line 17
}

Unfortunately, the logged message for an Exception is just "Uncaught exception" without the message that came with the Exception, so it might be helpful to create an Error and give it the same stack so the logged message will include the error message:

try {  // line 12
  document.querySelectorAll("div:foo");  // Throws a DOMException (invalid selector)
} catch(err) {
  e = new Error( "Here is some context -- " + err.message );
  e.stack = err.stack;
  throw e;  // line 17
}

Since the snippet output shows the line number of the re-throw, this confirms that the stack is preserved:

try {  // line 12
  try {  // line 13
    document.querySelectorAll("div:foo");  // Throws a DOMException (invalid selector)
  } catch(err) {
    console.log( "Stack starts with message: ", err.stack.split("\n")[0] );
    console.log( "Inner catch from:", err.stack.split("\n")[1] );
    e = new Error( "Here is some context -- " + err.message );  // line 18
    console.log( "New error from:", e.stack.split("\n")[1] );
    e.stack = err.stack;
    throw e;  // line 21
  }
} catch(err) {
  console.log( "Outer catch from:", err.stack.split("\n")[1] );
  throw err;  // line 25
}

4
  • 1
    Some errors, like DOMException, have read-only message properties.
    – Aaronius
    Commented Oct 14, 2020 at 21:50
  • @Aaronius, I've never seen that, and DOMException sounds like a browser things not a Node thing. But if that's a problem the next thing I'd do is make a new object that uses the original error as its prototype; I haven't tested this, but something like e = Object.create(err); e.message = "Here is some context -- " + err.message; throw e Commented Oct 15, 2020 at 1:55
  • 2
    That results in TypeError: Cannot set property message of which has only a getter. There might be a way to do it, but I haven't dug in enough. Your point about DOMException not being in Node is well-taken. I'll remove my downvote. There may be other types of errors in Node who's message property is similarly read-only though.
    – Aaronius
    Commented Oct 15, 2020 at 4:29
  • 1
    I just saw your latest updates. I'll need to give them a shot.
    – Aaronius
    Commented Oct 15, 2020 at 4:31
8

JavaScript has introduced the ability to create a new error, and tack on a "cause", causing the original stack trace to be preserved (as documented here). It looks like this:

try {
  someDangerousLogic();
} catch (originalError) {
  throw new Error(
    'Some additional, useful information',
    { cause: originalError }
  );
}

Complete example:

function someDangerousLogic() {
  throw new Error('Whoops!');
}

function main() {
  try {
    someDangerousLogic();
  } catch (originalError) {
    throw new Error(
      'Some additional, useful information',
      { cause: originalError }
    );
  }
}

main();

Unfortunately, not all browsers will display the error cause when the error goes uncaught. If I run the above "full example" in Chrome v110 then look at dev tools for the error, I'll only find the new stack trace (the "cause" is still accessible via the .cause property). Node, on the other hand, shows both the error and the cause when it goes uncaught:

Error: Some additional, useful information
    at main (/home/me/temp.js:9:11)
    at Object.<anonymous> (/home/me/temp.js:16:1)
    ... 5 lines matching cause stack trace ...
    at node:internal/main/run_main_module:17:47 {
  [cause]: Error: Whoops!
      at someDangerousLogic (/home/me/temp.js:2:9)
      at main (/home/me/temp.js:7:5)
      at Object.<anonymous> (/home/me/temp.js:16:1)
      at Module._compile (node:internal/modules/cjs/loader:1112:14)
      at Module._extensions..js (node:internal/modules/cjs/loader:1166:10)
      at Module.load (node:internal/modules/cjs/loader:988:32)
      at Module._load (node:internal/modules/cjs/loader:834:12)
      at Function.executeUserEntryPoint [as runMain] (node:internal/modules/run_main:77:12)
      at node:internal/main/run_main_module:17:47
}
2

you can also just continue to throw the error up your try chain. If you want to modify anything do so along the way: before the throw statement in b.

function a() {
    throw new Error('my message');
}

function b() {
    try {
        a();
    } catch (e) {
        // add / modify properties here
        throw e;
    }
}

function c() {
    try {
        b();
    } catch (e) {
        console.log(e);
        document.getElementById('logger').innerHTML = e.stack;
    }
}
c();
<pre id="logger"></pre>

0

You may want to have a look at the verror module from Joyent, which provides an easy way to wrap errors:

var originError = new Error('No such file or directory');
var err = new VError(originError, 'Failed to load configuration');
console.error(err.message);

This will print:

Failed to load configuration: No such file or directory
1
  • 2
    Does this preserve the call stack?
    – Flimm
    Commented May 23, 2020 at 21:31
0

2024, this worked for my promise: reject(new Error('Error while ...', { cause: error }))

return new Promise(async (resolve, reject) => {
  try {
    await this.dbSession('training_room').
      where('id', roomId).update(roomDetails)
    resolve();
  } catch (error) {
    reject(new Error('Error while ...', { cause: error }))
  }
})

Not the answer you're looking for? Browse other questions tagged or ask your own question.