Handling errors or exceptions is an integral part of software application development. Errors or exceptions can cause the program to work in an unexpected manner, resulting in security issues or inconvenience to the user. For this reason, it is very critical to handle errors appropriately.  

Let’s consider a simple software program like calculator performing division of two numbers. The software provides an input screen for users to enter the data on which the operation is required to be performed. Now, what if the user types in an invalid input, like say text instead of numbers or tries to perform division by zero. What happens if an error occurs?  

Any software is designed to perform orderly transitions between different states. Now because of errors, we might end up being in a state which is not considered or envisaged during software development. This situation is known as unpredictable state of the software and can lead to serious vulnerabilities such as loss of data, buffer overflows etc.  

Error handling, also called as exception handling, will help us prevent this unpredictable state by providing explicit instructions on handling unpredictable states. For example, if the user enters string data instead of numeric data for performing division operation, we can validate the data by transitioning to validation state for checking conditions that might create an error. If there is no error, software would continue to execute, otherwise it would enter a state where it would display the error message. 

Like most programming languages, JavaScript also has exception handling statements i.e. throw statement and try…catch statement. 

A note on exception Types: JavaScript provides predefined exceptions for specific purposes and it is suggested to use one of them for effectiveness. That said, any object can be thrown in JavaScript and the most common practice is to throw numbers or strings as errors.  

throw statement is used to throw an exception with a specified value to be thrown. A few examples are as follows: 

throw 'new error';  // String type 
throw 54;           // Number type 
throw false;        // Boolean type 
throw { errorcode: 500message: 'internal server error' } // Object type 

try…catch statement consists of a try block which contains multiple statements and a catch block containing the statements to be executed if an exception happens in try block during execution of statements in that block. 

In positive flow, all the statements in try block succeed and control will skip the catch block. Otherwise, if there is an exception thrown in the try block, the following lines in try block are skipped and the control shifts to catch. In either case, finally block executes after try and catch blocks are executed. 

function getIPLTeamCaptain(team){ 
  let teams = new Map(); 
  teams.set('CSK''Dhoni'); 
  teams.set('RCB''Kohli'); 
  teams.set('SRH''Warner'); 
  teams.set('MI''Rohit'); 
  teams.set('KXI''Rahul'); 
  teams.set('DC''Pant'); 
  teams.set('KKR''Morgan'); 
  teams.set('RR''Sanju'); 
  if (teams.has(team) === true) { 
    return teams.get(team); 
  } else { 
    throw 'InvalidTeam'; 
  } 
} 
try { 
  const captain = getIPLTeamCaptain('Pune'); 
  console.log(captain); 
catch (error) { 
  console.error(error); 
} 

In catch block, we have an identifier that holds the value specified by the throw statement. This can be used to get the information about the exception details. The scope of this identifier lasts within catch block and post finishing the execution, the identifier no longer exists. 

Note on best practices: When logging errors to the console inside a catch block, it is advisable to use console.error() rather than console.log() for debugging. It formats the message as an error, and adds it to the list of error messages generated by the page. 

Few pitfalls or best practices to be known while considering finally block 

  • If there is an I/O operation in try block which results in an exception, then it is better to handle the clean-up code in finally block. For example, a DB connection is made or a file is opened and during processing of the file an exception has occurred. Though the catch will handle the exception, clean up code has to be handled in finally block i.e. closing of file. 
  • The finally block will execute regardless of whether an exception has been raised or not.  If a finally block returns a value, then regardless of whether an exception is raised or a value is returned in the try and catch block, the finally block return value will overwrite those values. 
function raiseException() { 
  try { 
    throw 500; 
  } catch(e) { 
    console.error(e); 
    throw e// this throw statement is suspended until finally block has completed 
  } finally { 
    return false// overwrites the previous "throw" 
  } 
  // always returns false 
} 
try { 
  raiseException(); 
catch(e) { 
  // this is never reached! 
  // while raiseException() executes, the `finally` block returns false, which overwrites the `throw` inside the above `catch` 
  console.error(e); 
} 

Nesting of try..catch statements is possible; but if inner try block does not have corresponding catch block then it should have finally block. The enclosing try…catch statement’s catch block is checked for a match. 

Error objects are specific type of core objects which are thrown during runtime. To log more refined messages about error, we can use ‘name’ and ‘message’ properties. 

Refer to this link to know more about Error object

For example, throw new Error(‘Error Message’); when caught in catch block, the identifier will have the property name as ‘Error’ and property message as ‘Error Message’. 

When a runtime error occurs, error objects are created and thrown. Any user-defined exceptions can be built extending the Error. 

Below are some of the predefined error constructors extended from generic Error constructor.  

  • AggregateError  
  • EvalError  
  • RangeError  
  • ReferenceError  
  • SyntaxError  
  • TypeError  
  • URIError  

Let’s understand each one with an example. 

1. AggregateError as the name suggests is used to wrap multiple errors into a single error. Imagine we are processing multiple async calls via promises and used Promise.any(). This would raise AggregateError or we can create our own new AggregateError as shown below. 

Promise.any([ 
  Promise.reject(new Error("some error")), 
]).catch(e => { 
  console.log(e instanceof AggregateError); // true 
  console.log(e.message);                   // "All Promises rejected" 
  console.log(e.name);                      // "AggregateError" 
  console.log(e.errors);                    // [ Error: "some error" ] 
}); 

OR 

try { 
  throw new AggregateError(['Error'500new Error('message')],'AggregateErrorMessage'); 
catch (e) { 
  console.log(e instanceof AggregateError); // true 
  console.log(e.message);                   // "AggregateErrorMessage" 
  console.log(e.name);                      // "AggregateError" 
 console.log(e.errors.length)              // 3 
} 

2. EvalError occurs when using global eval() function. This exception is no more thrown by JavaScript. 

3. RangeError is to be thrown when a value is not in the range of allowed values. Like passing bad values to numeric methods like toFixedtoPrecision etc. 

try { 
  var num = 10.123.toFixed(-1); 
catch (e) { 
  console.error(e instanceof RangeError); // true 
  console.error(e.message);               // argument must be between 0-100 
  console.error(e.name);                  // RangeError 
} 

4. ReferenceError is raised when a non-existent variable is referenced. 

try { 
  let a = undefinedVar 
catch (e) { 
  console.log(e instanceof ReferenceError)  // true 
  console.log(e.message)                    // "undefinedVar is not defined" 
  console.log(e.name)                       // "ReferenceError" 
} 

5. SyntaxError is thrown by the JavaScript engine if it encounters tokens which do not conform to the syntax of the language while parsing the code. 

try { 
  eval('hoo bar'); 
catch (e) { 
  console.error(e instanceof SyntaxError); // true 
  console.error(e.message);                // Unexpected identifier 
  console.error(e.name);                   // SyntaxError 
} 

6. TypeError is raised if an operation is performed on a value which is not expected on its type. Say if we are attempting to modifa value that cannot be changed. 

try { 
  const a = "constant"; 
  a = 'change'; 
catch (e) { 
  console.log(e instanceof TypeError)  // true 
  console.log(e.message)               // "Assignment to constant variable." 
  console.log(e.name)                  // "TypeError" 
} 

7. URIError is raised when global URL handling function is used in an inappropriate way. 

try { 
  decodeURIComponent('%') 
catch (e) { 
  console.log(e instanceof URIError)  // true 
  console.log(e.message)              // URI malformed 
  console.log(e.name)                 // "URIError 
}  

Handling Specific error

If we want to handle different error types differently then we can check for error instanceof and handle them accordingly. 

try { 
  customMethod(); 
catch (e) { 
  if (e instanceof EvalError) { 
    console.error(e.name + ': ' + e.message) 
  } else if (e instanceof RangeError) { 
    console.error(e.name + ': ' + e.message) 
  } else if (e instanceof URIError) { 
    console.error(e.name + ': ' + e.message) 
  } else if (e instanceof TypeError) { 
    console.error(e.name + ': ' + e.message) 
  } else if (e instanceof AggregateError) { 
    console.error(e.name + ': ' + e.message) 
  } else if(typeof e === 'string' || e instanceof String) { 
    console.error(`Error of type string with message ${e}`); 
  } else if(typeof e === 'number' || e instanceof Number) { 
    console.error(`Error of type number with message ${e}`); 
  } else if(typeof e === 'boolean' || e instanceof Boolean) { 
    console.error(`Error of type boolean with message ${e}`); 
  } else { 
    // if we aren't sure of other types of errors, 
    // probably it is best to rethrow 
    throw e; 
  } 
} 

Note: In real time applications, we would handle specific error types based on the functionality in the try block and wouldn’t have so many different error types for a single try…catch block. Also having multiple errors and having multiple if … else statements is not recommended as per the single responsibility principle. 

Please refer to the above code only for reference to identify the specific error type. 

Custom Error Type

Sometimes we would like customize the error or want to have our own error types. To create custom Error types, we have to extend the existing Error object and throw the custom error accordingly. We can capture our custom error type using instanceof. This is cleaner and more consistent way of error handling. 

class MyError extends Error { 
  constructor(customProperty, ...params) { 
    // Pass arguments to parent constructor 
    super(...params); 
    // Maintains proper stack trace for where our error was thrown(available on V8) 
    if (Error.captureStackTrace) { 
      Error.captureStackTrace(thisMyError); 
    } 
    this.name = 'MyError'; 
    // Custom debugging information 
    this.customProperty = customProperty; 
    this.date = new Date(); 
  } 
} 
try { 
  throw new MyError('custom''message') 
catch(e) { 
  console.error(e.name)           // MyError 
  console.error(e.customProperty// custom 
  console.error(e.message)        // message 
  console.error(e.stack)          // MyError: message with stacktrace 
} 

DOMException Type

The DOMException occurs as a result of calling a method or accessing a property of a web API which represents an abnormal event that occurred. 

Refer to this link to know more about DOMException 

Promise Error Propagation

A Promise is an object representing the eventual completion or failure of an asynchronous operation. Promises allow to chain multiple asynchronous operations back-to-back in a sequence order. 

When we chain multiple asynchronous operations, then we can have catch at the end of the chain to capture any exception or rejection that happened at any promise. Catch takes a callback function and the callback is set to execute when the Promise is rejected. 

For example: 

Promise.resolve(doSomething) 
.then(action1 => Action1(action1)) 
.then(action2 => Action2(action2)) 
.catch(failureCallback); 
// Attaches a callback for only the rejection of the Promise. 
// @returns — A Promise for the completion of the callback. 

As per the above example, we are performing multiple async actions but we have a single catch block to capture any rejection. Let’s understand this with an assumption that the calls are synchronous in nature. 

try { 
  const action1 = doSomething(); 
  const action2 = Action1(action1); 
  const finalResult = Action2(action2); 
  console.log(`Got the final result: ${finalResult}`); 
catch(error) { 
  failureCallback(error); 
} 

If there’s an exception, then the browser will jump over to catch block and execute the failureCallback. 

Based on ECMAScript 2017 async/await syntactic sugar-coated way, we can change the synchronous code to asynchronous. 

async function SampleAsync() { 
  try { 
    const action1 = await doSomething(); 
    const action2 = await Action1(action1); 
    const finalResult = await Action2(action2); 
    console.log(`Got the final result: ${finalResult}`); 
  } catch(error) { 
    failureCallback(error); 
  } 
}  

Promises help to resolve the callback hell by catching all errors i.e. thrown exceptions or programming errors. This is important for functional composition of async operations. 

What would happen if there isn’t a catch for a promise?

 Promises are associated with two events i.e. rejectionhandled and unhandledrejection. Whenever a promise is rejected, one of these two events is sent to the global scope which might be a window or a worker. 

  • rejectionhandled – sent when a promise is rejected, and post rejection the executor’s reject is handled. 
  • unhandledrejection – promise is rejected but there is no rejection handler available. 

Note: Both events are of type PromiseRejectionEvent which has details of the actual promise which was rejected and a reason property depicting the reason for rejection. 

Since both the events are in global scope, all errors will go to the same event handlers regardless of the source. This makes it easy to offer a fallback error handling mechanism for promises. 

While working with Node.js on the server side, we might include some common modules and there is a high possibility that we may have some unhandled rejected promises which get logged to the console by the Node.js runtime. If we want to capture those and process or log them outside the console, then we can add these handlers on the process as shown below. 

process.on("unhandledRejection", (reasonpromise=> { 
  // You might start here by adding code to examine the "promise" and "reason" values. 
}); 

 The impact of adding this handler on the process is that it will prevent the errors from being logged to the console by the node.js runtime. There is no need for calling preventDefault() method available on browser runtimes. 

 Note: Adding the listener on the process.on and not coding to capture the promise and reasons would result in loss of the error. So it is suggested to add code to examine the rejected promise to identify the cause of rejection. 

Window UnhandledRejection Event

The global scope being window (may also be worker), it is required to call the event.preventDefault() in the unhandledrejection to cancel the event. This prevents it from bubbling up to be handled by the runtime’s logging code. It works because unhandledrejection is cancellable. 

window.addEventListener('unhandledrejection'function (event) { 
  // ...your code here to handle the unhandled rejection... 
  // Prevent the default handling (such as outputting the 
  // error to the console) 
  event.preventDefault(); 
}); 

To summarise a few points related to error handling: 

  • Try to handle errors specific to the type of error. 
  • Use Error object instead of numeric, string or Boolean while throwing an error. As Error object will capture the stack trace and error type, specific handling is possible. 
  • Release resources if any in finally block. 
  • Being more specific with throw error helps in handling it cleanly instead of generic error. 
  • Avoid unwanted catch block if they are meant only for logging, and re-throw the error again. Otherwise, this will result in clogging of logs. 
  • Last but not least, suppression/swallow/shadowing of exception as shown below should be avoided. This might make the application continue to work but the underlining issue will not be fixed. 
try { 
  throw new Error('my error'); 
} 
catch(e) {} // no one knows about this 
try { 
  throw new Error('my error'); 
} 
catch(e) { 
  console.log(err); // error is logged but application continues 
} 
try { 
  throw new Error('my error'); 
} 
finally { 
  return null// error is swallowed 
} 
try { 
  throw new Error('actual error'); 
} 
finally { 
  throw new Error('new error'); // actual error is shadowed by new one 
} 

Conclusion 

Error handling is one of the most important facets in software programming that has to be acknowledged and experienced by the developer. Software should be as predictable as possible in terms of state transitions.