While it is okay possible to throw any value, it is generally considered poor form to throw anything other than an instance of Error or one of its subclasses. There are several reasons for this:
Catching code may expect the thrown object to have the usual message, stacktrace, and name properties that appear on Errors.
Lack of a stacktrace makes debugging problematic, especially in the case of uncaught exceptions / unhandled rejections. E.g. Debugging an "Uncaught [Object object]" error can be particularly painful.
A string is not an error object, and does not convey any useful debugging information. Devtools rely on that, such as the file and line where the error was created, the stacktrace at the throw location etc, which are available as properties on Error objects.
Whenever you think of throwing a primitive string value, throw a new Error("<the string>") instead.
As others have mentioned above, if you are not throwing an Error object, then you must have try/catch blocks to trap these objects and handle them appropriately or else be in a world of hurt for debugging.
However, when it comes to throwing Errors for non-error handling purposes like controlling program flow, this may be a helpful way to utilize throw without an Error.
When using throws to control program flow, it can be inefficient in any language as the runtime will often do a lot of heavy lifting to unwind call stack information and serialize the data so its available to the user land scope. By avoiding Error creation, you can avoid this performance hit. The key is that you must have a handler up the call stack that knows how to handle this situation. For instance if you throw {isHardStop: true, stopCode: SOME_CODE} and design the handlers to detect this, you may be able to flatten out some of your code or choose cleaner syntax.
Your handler for this ladder case could be structured like:
try { ... } catch(thr) {
if(!thr){
// Is not Error or Json - Handle accordingly
} else if(thr.isHardStop){
// Handle the stop
} else {
// Most likely a real error. Handle accordingly
}
}
Although you can throw any type of data that you’d like, this is not optimal when debugging. A JS Error object contains all kind of information regarding the Error and a message. Whereas a string can only contain a message.
This additional information includes:
fileName: from which file the error was thrown
Linenumber: from which line the error was thrown
stacktrace: from which function the error was called
Here is for example a stacktrace from chrome devtools: