Should I use console.error() or throw new Error()

Viewed 7338

I've seen both:

throw new Error(error);

&

console.error(error);

E.G:

jQuery:

                if ( !w.document ) {
                    throw new Error( "jQuery requires a window with a document" );
                }

&
Vue.js:

      if (config.warnHandler) {
        config.warnHandler.call(null, msg, vm, trace);
      } else if (hasConsole && (!config.silent)) {
        console.error(("[Vue warn]: " + msg + trace));
      }

Both error handling ways seem reliable and used. But my question is:

Is there a difference between them? And if there is, when should I use which?

4 Answers

The key difference: throwing halts the execution, while console.error does not.

Most of the time, it's better to throw an error.

That's a built-in way to signal that something failed and normal execution cannot continue unless the error is expected, caught and handled properly.

In the most platforms, an uncaught exception will be logged to the console as well to warn the developer about it, but caught exceptions won't be logged, since they are assumed to be handled by the code.

Using console.error can be good for cases where the error happened isn't fatal, but you'd like to warn the developer.

However, overusing this feature can easily cause other errors and harder-to-debug code. For example, consider this code:

const elem = document.querySelector('.elem')
if(!elem) 
  console.error('elem cannot be found!')
const returnValue = functionThatDoesSomethingWithElem(elem)
if(!returnValue.success) 
  console.error('Doing something with elem has failed!')
if(!returnValue.doSomethingElse()) 
  console.error('Doing something else with elem has failed!')

The above code will log three errors if there's no elem, but execution still continues, maybe causing even more errors.

By throwing an exception, this is avoidable:

const elem = document.querySelector('.elem')
if(!elem) 
  throw new Error('elem cannot be found!')
const returnValue = functionThatDoesSomethingWithElem(elem)
if(!returnValue.success) 
  throw new Error('Doing something with elem has failed!')
if(!returnValue.doSomethingElse()) 
  throw new Error('Doing something else with elem has failed!')

This will print only the first error message, and execution halts, unless you put it inside a try..catch structure, like:

try{
  const elem = document.querySelector('.elem')
  if(!elem) 
    throw new Error('elem cannot be found!')
  const returnValue = functionThatDoesSomethingWithElem(elem)
  if(!returnValue.success) 
    throw new Error('Doing something with elem has failed!')
  if(!returnValue.doSomethingElse()) 
    throw new Error('Doing something else with elem has failed!')
}catch(exception){
  console.error(exception)
  fallbackMethod()
}

There's another difference: thrown errors can be caught by the caller of your function, so it can programmatically handle them (causing the execution to continue, and the error isn't displayed). On the other hand, if you use console.error, the caller can not decide if an error is expected, causing to log the error even if it is normal, so the console can become messy (you can't decide what's a real error and what's not.

Those two are completely different, let's see documentation of each one:

console.error:

Outputs an error message to the Web Console.

Throw:

throws a user-defined exception. Execution of the current function will stop (the statements after throw won't be executed), and control will be passed to the first catch block in the call stack. If no catch block exists among caller functions, the program will terminate.

So, as you can see, they work differently, you can use both in your code, so keep in mind that just a console.error will probably pass unseen by the user, only those with console open will see the message, while throw will be noted, since everything will stop.


Let's see some functional examples:

  • console.error

Note that the "custom" error will show on console, but code stills executing, you only see the error message because console is showing here in stackOverflow. To stop execution you would need a return right after the console.error

function changeString(currentString, append) {
  if (typeof append != 'string') {
    console.error("Cannot change string!");
  }

  //THIS PART BELOW WILL EXECUTE, even with error logged
  console.log(currentString + append);

}

changeString("I will show up even with error logged ", null)


  • Throw

Note that code stops execution when error is thrown and also the message of the error is different from the one that console.error shows.

function changeString(currentString, append){
  if (typeof append != 'string'){
    throw new Error("Cannot change string!");
  } 
  
  //THIS PART BELOW WONT EXECUTE
  console.log(currentString + append);
  
}

changeString("I wont show up in the console", null)

Here I added a try/catch, note the catch block being executed, this way I have control over the error and what should happen after the error being thrown, also, I mixed both: Throw and console.error

function changeString(currentString, append) {
  try {
    if (typeof append != 'string') {
      throw new Error("Cannot change string!");
    }
    
    console.log(currentString + append);
    
  } catch (er){
    console.error("I'm being logged from inside a catch block because an error was thrown");
    //DO WHATEVER YOU NEED TO FIX THE ERROR AND ALERT THE USER HERE
  }
}

changeString("I wont show up in the console", null)


Further read on official documentations:
Throw
console.error

I would not consider them to be interchangeable. Console.error() simply logs your error to the console with special formatting.

When you actually throw an error you are essentially terminating execution unless you catch the error using a try catch block or some other form of error handling.

Why not have both, like this:

throw new ConsoleError('bad thing');

Source:

// based on https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Error
class ConsoleError extends Error {
  constructor(...args) {
    const returnValue = super(...args);
    console.error(...args);
    // Maintains proper stack trace for where our error was thrown (only available on V8)
    if (Error.captureStackTrace) {
      Error.captureStackTrace(this, ConsoleError);
    }

    this.name = 'ConsoleError';
    this.message = args[0];
    // Pass remaining arguments (including vendor specific ones) to parent constructor
    return returnValue;
  }
}
window.ConsoleError = ConsoleError;

You may also want show the user when some errors happen, for this, use

// this to avoid crashing:
window.toastError('nice user error message', 'extra developer data:', someObject)

// this to crash:
throw new ToastError('we made a mistake')

Source:

window.toastError = (userMsg, ...developerDebugArgs) => {
  // TODO: implement toast notifications
  // only use userMsg for the toast notification
  // Using DOMPurify and rendering userMsg as (potential) html may be nice,
  // but backend team may prefer to simple return plain text instead of html
  // may need to de-dupe messages being sent to toast component
  console.log(
    `%c${userMsg}`,
    'color: orangered;font-style:italic;',
    ...developerDebugArgs
  );
};

class ToastError extends Error {
  constructor(...args) {
    const returnValue = super(...args);
    window.toastError(...args);
    // Maintains proper stack trace for where our error was thrown (only available on V8)
    if (Error.captureStackTrace) {
      Error.captureStackTrace(this, ToastError);
    }

    this.name = 'ToastError';
    this.message = args[0];
    // Pass remaining arguments (including vendor specific ones) to parent constructor
    return returnValue;
  }
}
window.ToastError = ToastError;

You could also rename the ToastError class ToastException instead, if it suits your fancy

Related