Handle error in JS

Errors are the rare things that we often dismiss

No matter how great we are at programming, sometimes our scripts have errors. They may occur because of our mistakes, an unexpected user input, an erroneous server response, and a thousand other reasons

Handling errors

There're some common pattern to handle errors

  • Returing null;

  • Throwing exceptions;

  • Returning exception;

  • The Option type

Returning null

function parse(birthday: string): Date | null 
{ 
    let date = new Date(birthday)
        if (!isValid(date)) {
            return null
         }
    return date; 
}
// Checks if the given date is valid
function isValid(date: Date) {
      return Object.prototype.toString.call(date) === '[object Date]'
          && !Number.isNaN(date.getTime())
    }
}
💡
Return null can be verbose that having to check for null after every operation can become verbose as you start to nest and chain operations.

Throwing exceptions

function parse(birthday: string): Date | null 
{ 
    let date = new Date(birthday)
        if (!isValid(date)) {
            throw new RangeError("Enter a date in the form YYYY/MM/DD");
         }
    return date; 
}

try {
    let date = parse(ask())
     console.info('Date is', date.toISOString())
    } catch (e) {
     if (e instanceof RangeError) 
        { console.error(e.message)
      } 
     else { throw e; }
}
💡
The good point of throwing our own error instead of returning null is that we can have the error message explains why it's failed. But lackof the error definition when handle it at catch level :)

Returning error

TypeScript isn’t Java, and doesn’t support throws clauses.1 But we can achieve some‐ thing similar with union types

function parse( birthday: string): Date | InvalidDateFormatError | DateIsInTheFutureError 
{ 
    let date = new Date(birthday)
    if (!isValid(date)) {
    return new InvalidDateFormatError('Enter a date in the form YYYY/MM/DD')
    }
    if (date.getTime() > Date.now()) 
    {
    return new DateIsInTheFutureError('Are you a timelord?')
    }
}

let result = parse(ask()) 
// Either a date or an error if 
if (result instanceof InvalidDateFormatError) {
      console.error(result.message)
    } else if (result instanceof DateIsInTheFutureError) 
    { 
      console.info(result.message) } 
     else {
      console.info('Date is', result.toISOString())
    }
  • Encode likely exceptions in parse’s signature.

  • Communicate to consumers which specific exceptions might be thrown.

  • Force consumers to handle (or rethrow) each of the exceptions.

💡
When we throw the exception, TS have no idea what can be thrown because we can pretty much throw anything

The try-catch syntax

try {
   // code
} catch (err) {
  // error handling
}

There's a diagram to show us how it work

try...catch works synchronously

If an exception happens in "schedule" code, like in setTimeout, then try catch won't work

try {
   setTimeout(function(){
    noSuchVariable; // script will die here
   },1000)
} catch(err) {

}
💡
That is because the function itself is executed later, when the engine has already left the try-catch construct
setTimeout(function() {
  try {
    noSuchVariable; // try...catch handles the error!
  } catch {
    alert( "error is caught here!" );
  }
}, 1000);
💡
Is it a good practice when we always have to catch an error every time we write code?

Error object

When an error occurs, JavaScript generates an object containing the details about it. The object is then passed as an argument to catch

For all built-in errors, the error object has two main properties:

  • name (error name). For instance, an undefined variable that's "ReferenceError"

  • message (textual message for about error detail)

  • stack (current callstack; this shows us where the error happened)

💡
It's very similar structure on how BE sends error feedback to FE
try {
  lalala; // error, variable is not defined!
} catch (err) {
  alert(err.name); // ReferenceError
  alert(err.message); // lalala is not defined
  alert(err.stack); // ReferenceError: lalala is not defined at (...call stack)
  // Can also show an error as a whole
  // The error is converted to string as "name: message"
  alert(err); // ReferenceError: lalala is not defined
}
💡
The purpose of error objects guide developers to solve the problem

Throwing our own error

let json = '{ "age": 30 }'; // incomplete data

try {
  let user = JSON.parse(json); // <-- no errors
  alert( user.name ); // no name!
} catch (err) {
  alert( "doesn't execute" );
}

Build-in errors in JS

We have a few build-in constructors for standard errors:

  • Error

  • SyntaxError

  • ReferenceError

  • TypeError

  • URIError

  • RangeError

//Syntax Error
let x = 5;
console.log(x)

//Reference Error
console.log(variableDoesNotExist);
Uncaught ReferenceError: variableDoesNotExist is not defined
    at <anonymous>:1:13

//Type Error
const obj = null;
null.undefined; 
VM42:4 Uncaught TypeError: Cannot read properties of null 
(reading 'property') at <anonymous>:4:17

//URI Error
decodeURIComponent('%');
VM42:1 Uncaught URIError: URI malformed
    at decodeURIComponent (<anonymous>)
    at <anonymous>:1:2
(anonymous) @ VM42:1

Of course, JS is not going to hand-feed us because the use-case support to cover every single scenario is impractical. If we want to expand this to handle some more special cases, we can extend the current error object.

Handle HTTP error from server to Error in JS

Design system

After we create our HTTPError, we can then use it to handle error for our request to server

export async function apiCall(path, init) {
  let response;
  let json;
  try {
    const jsonRespInfo = await getJSON(`/api/${path}`, init);
    response = jsonRespInfo.response;
    json = jsonRespInfo.json;
  } catch (err) {
    if (err instanceof HTTPError) throw err;
    throw new Error(
      stringifyError(
        `Networking/apiCall: An error was encountered while making api call to ${path}`,
        err,
      ),
    );
  }
  if (!response.ok)
    throw new HTTPError(response, 'Problem while making API call');
  return json;
}
💡
The HTTPError class needs the response information from the server to construct its own object, and after creating a new HTTPError object, it will throw the object for the responsible users to handle it gracefully

Handle error with unknown

try {
    // technically we can throw anything
    throw "new string";
} catch(err) {
// in this case because of string wrapper it will be undefined but
// you can mess it :)
    err.stack; 
}

try {

} catch(err:unknown) {
    if(isError(err)) {
        // best is to use type-guard to catch it
        console.log(err);
    }
}

Global catch

Let’s imagine we’ve got a fatal error outside of try...catch, and the script died. Like a programming error or some other terrible thing. We can have another extra layer of protection in case we forget to handle it somehow

window.onerror = function(message,url,line,col,error) {
    // something has happened
}
💡
In reactJS , we have something similar called GlobalErrorBoundary component, and we'll cover it later

Centralize errors on ReactJS

class GlobalErrorBoundary extends React.Component {
  constructor(props) {
    super(props);
    this.state = { hasError: false };
  }

  static getDerivedStateFromError(error) {
    return { error: error.stack, hasError: true };
  }

  componentDidCatch(error, errorInfo) {
    console.warn(error, errorInfo);
    const stack = error.stack;
    let errorType = '';

    if (stack === null) {
      errorType = stack.substring(0, stack.indexOf(':'));
    }
    this.props.tracker(EVENT_TRACKING_JAVASCRIPT_ERROR, {
      'Error message': error.message,
      'Error stack': error.stack,
      'Error type': errorType,
      errorInfo
    });

    serverLoggerKibana.log(
      this.props.api,
      this.props.configuration,
      getAppInfo(),
      SERVER_LOGGER_REACT,
      'error',
      error,
      errorInfo
    );
  }

  render() { 
     //Render a component to show a pop-up error to users
  }
💡
We indeed sent the error to the server to log it if we can't handle it on client side

Custom errors, extending Error

When we develop something, we often need our own error classes to reflect specific things that may go wrong in our tasks. For errors in network operations we may need HttpErrordatabase operations DbError, for searching operations NotFoundError and so on.

Our errors should support basic error properties like message, name and, preferably, stack. But they also may have other properties of their own, e.g. HttpError objects may have a statusCode property with a value like 404 or 403 or 500.

💡
We can transform the feedback error from server to UI error by extending the error object from JS

Let's explore Error class

// The "pseudocode" for the built-in Error class defined by JavaScript itself
class Error {
  constructor(message) {
    this.message = message;
    this.name = "Error"; // (different names for different built-in error classes)
    this.stack = <call stack>;
}

Write our own ValidationError class to support validation error

class ValidationError extends Error {
  constructor(message) {
    super(message);
    //override it
    this.name = "ValidationError";
  }
}

// Usage
function readUser(json) {
  let user = JSON.parse(json);

  if (!user.age) {
    throw new ValidationError("No field: age");
  }
  if (!user.name) {
    throw new ValidationError("No field: name");
  }

  return user;
}

// Working example with try..catch

try {
  let user = readUser('{ "age": 25 }');
} catch (err:unknown) {
  if (err instanceof ValidationError) {
    alert("Invalid data: " + err.message); // Invalid data: No field: name
  } else if (err instanceof SyntaxError) { // (*)
    alert("JSON Syntax Error: " + err.message);
  } else {
    throw err; // unknown error, rethrow it (**)
  }
}

Tasks

Create a class FormatError that inherits from the built-in SyntaxError class.

It should support message, name and stack properties.

class FormatError extends SyntaxError {
    // we have to pass in message to super
    // and write our own constructor name
    constructor(message) {
       super(message);
       this.name = this.constructor.name;
    }
}

let err = new FormatError("formatting error");

alert( err.message ); // formatting error
alert( err.name ); // FormatError
alert( err.stack ); // stack

alert( err instanceof FormatError ); // true
alert( err instanceof SyntaxError ); // true (because inherits from SyntaxError)

Summary

  • We can inherit fromError and other built-in error classes normally. We just need to take care of the name property and don’t forget to call super.

  • We can use instanceof to check for particular errors. It also works with inheritance. But sometimes we have an error object coming from a 3rd-party library and there’s no easy way to get its class. Then name property can be used for such checks.

  • Wrapping exceptions is a widespread technique: a function handles low-level exceptions and creates higher-level errors instead of various low-level ones. Low-level exceptions sometimes become properties of that object like err.cause in the examples above, but that’s not strictly required.