Handling javascript exceptions

Table of contents

Mistakes happen everywhere, including in javascript code. Today we explore how to deal with (un)expected errors (aka Exceptions). We look at how to deal with them, why you need the try / catch / finally syntax and how to throw your own errors.

A sample app

We will approach the topic by creating a quick sample app and making changes to it along the way. The initial state would look something like this:

index.html

<!Doctype html>
<html>
   <body>
      <input type="text" placeholder="json data">
      <button>Submit</button>
      <script type="application/javascript">
         let input = document.querySelector("input")
         let button = document.querySelector("button")
         document.querySelector("button").onclick = async()=>{   

            // set some visual loading state
            // and prevent further clicks while working
            button.disabled = true
            button.innerText = "processing..."

            // simulate a long-running task by waiting 2s
            await new Promise(r => setTimeout(r, 2000))

            //parse input & print age
            let user = JSON.parse(input.value)
            let age = parseInt(age)
            console.log(`You are ${age} years old`)

            // undo loading state, do cleanup and enable button
            button.disabled = false
            button.innerText = "Submit"
            input.value = ""
         }
      </script>
   </body>
</html>

Given valid input like {"age": 30}, this little web page will take an arbitrary json string, set some visual loading indicators in the html markup, wait 2s to simulate a longer-running task, print the user's age and remove the loading indicator.

Catching an exception

As long as the input provided by the user is a valid json string, the code above will work just fine. But what if we put in something that's clearly not json, like }}}bad value? After the 2s timeout, our console will throw an error similar to this:

On top of that, our button is now stuck in the "processing..." state and can't be clicked anymore, effectively soft-locking the app into an unusable state. This happens because the function JSON.parse() threw a SyntaxError exception that was not caught by us. As soon as it did, execution stopped and it skipped all lines below it, so our cleanup never ran.

We can adjust our javascript code to catch the exception instead:

let input = document.querySelector("input")
let button = document.querySelector("button")
document.querySelector("button").onclick = async()=>{   

    // set some visual loading state
    // and prevent further clicks while working
    button.disabled = true
    button.innerText = "processing..."

    try{
        // simulate a long-running task by waiting 2s
        await new Promise(r => setTimeout(r, 2000))

        //parse input & greet user
        let user = JSON.parse(input.value)
        let age = parseInt(user.age)
        console.log(`You are ${age} years old`)
    }catch(err){
        console.warn(`Ignoring invalid JSON input: ${err}`)
    }

    // undo loading state, do cleanup and enable button
    button.disabled = false
    button.innerText = "Submit"
    input.value = ""
}

We have now isolated the call to JSON.parse() in a try block and, in case it throws an exception again, handle it with the catch block. The code inside the try block will stop at any line that throws an exception and immediately jump to the catch block, skipping any remaining lines in the try block. That's why in our example, the console.log(`Hello ${user.name}`) line won't run if JSON.parse() throws an exception. Code after the catch block runs normally regardless of whether the statements in try succeeded or catch has caught an exception, so our cleanup always runs and we aren't soft-locked on errors anymore.

Throwing your own exceptions

Now that we have seen how to deal with exceptions thrown by others, let's join in on the fun. We throw exceptions when we encounter a situation we consider to be an error. In our example, even json input like {"age": "horse"} will run and print "Hello NaN" to console. But from our perspective, a user without a valid number as an age would be an error, right?

To handle this, we can check and throw our own error if a user's age can't be parsed as an integer:

if(isNaN(age)){
    throw "Age is not a number"
}

We can throw any variable we want, like strings, numbers or objects. For now we just throw a simple error message string.

Why you should throw Error objects

Now that we have seen how easy it is to throw a string, why would we want to throw more complex data types like the Error type? Because it provides a lot of built-in debugging information in addition to our error message, like the name of the file the exception happened in, the line and column within that file:

Adjusting our code to throw an Error type is not much effort, but the benefits are obvious:

if(isNaN(age)){
    throw new Error("Age is not a number")
}

Handling exceptions selectively

Now that we have seen how to throw a single exception, let's make the example a little more complex. Assume that in addition to checking if the age is a number, we also throw another exception for users who are less than 18 years old. Let's say our application has some fancy code somewhere else to check for parental permission if the user is a minor but our application doesn't. This creates a problem: how do we distinguish between the exceptions that were thrown?

The solution is to create our own Error types by extending the builtin Error type:

class TooYoungError extends Error {}
class InvalidAgeError extends Error {}

if(isNaN(age)){
    throw new InvalidAgeError("age is not a number")
}
if(age < 18){
    throw new TooYoungError("user is younger than 18")
}

This simple line of code enables us to still use the Error object under the hood and have all the debugging information for free while throwing a custom error so our catch can distinguish between the types of exceptions it handles:

catch(err){
    if(err instanceof InvalidAgeError || err instanceof SyntaxError){
        // handle bad json inputs
        console.log(`Ignoring invalid json input: ${err}`)
    }else{
        // let someone else deal with age issues
        throw err
    }
}

The code now checks what type the error message is using type instanceof and still ignores invalid age values or malformed json. But if it encounters someone too young for our application it will throw the err value again. Throwing an exception from a catch block is called re-throwing. Be re-throwing, our catch block gives responsibility for handling the exception to the function that called the current function (or it's parent, ....) until one of them either handles the error or it arrives unhandled in the main scope, where it would crash our javascript process once more.

Let's add a catch block around the entire onclick event to catch the re-thrown error as well:

let input = document.querySelector("input")
let button = document.querySelector("button")

// define custom error types
class InvalidAgeError extends Error {}
class TooYoungError extends Error {}

document.querySelector("button").onclick = async()=>{   

    // wrap onclick event handler into try block to catch re-thrown exceptions
    try{

        // set some visual loading state
        // and prevent further clicks while working
        button.disabled = true
        button.innerText = "processing..."

        try{
            // simulate a long-running task by waiting 2s
            await new Promise(r => setTimeout(r, 2000))

            //parse input & greet user
            let user = JSON.parse(input.value)
            let age = parseInt(user.age)
            if(isNaN(age)){
                throw new InvalidAgeError("age is not a number")
            }
            if(age < 18){
                throw new TooYoungError("user is younger than 18")
            }
            console.log(`You are ${age} years old`)
        }catch(err){
            if(err instanceof InvalidAgeError || err instanceof SyntaxError){
                // handle bad json inputs
                console.log(`Ignoring invalid json input: ${err}`)
            }else{
                // let someone else deal with age issues
                throw err
            }
        }
        // undo loading state, do cleanup and enable button
        button.disabled = false
        button.innerText = "Submit"
        input.value = ""
    
    // catch re-thrown exception
    }catch(err){
        console.log(`Caught global exception ${err}`)
    }
}

We can now differentiate between exception types and handle them based on what they are (or re-throw the ones we don't want to handle).

What's the finally block for?

So if try and catch deal with exceptions, why is there a finally block as well? The answer to that is not immediately obvious, but can lead to nasty bugs if not understood. Remember when we said that throw will stop the execution and skip all following code until it is caught by a catch block? Well, what happens if we re-throw an exception from a catch block? It does the same thing! In our example, this bug exists as well: if a user's age is less than 18, our cleanup code won't run again and the soft-lock error is back!

This is where the finally block comes to our rescue: it will always run, regardless of what happens inside it's try or catch blocks - even if catch re-throws an exception, finally will still execute before another catch block handles that re-thrown exception.

Editing our application to include the cleanup in a finally block, it will now look like this:

<!Doctype html>
<html>
   <body>
      <input type="text" placeholder="json data">
      <button>Submit</button>
      <script type="application/javascript">
         let input = document.querySelector("input")
         let button = document.querySelector("button")

         // define custom error types
         class InvalidAgeError extends Error {}
         class TooYoungError extends Error {}

         document.querySelector("button").onclick = async()=>{  

            // wrap onclick event handler into try block to catch re-thrown exceptions
            try{

               // set some visual loading state
               // and prevent further clicks while working
               button.disabled = true
               button.innerText = "processing..."

               try{
                  // simulate a long-running task by waiting 2s
                  await new Promise(r => setTimeout(r, 2000))

                  //parse input & greet user
                  let user = JSON.parse(input.value)
                  let age = parseInt(user.age)
                  if(isNaN(age)){
                     throw new InvalidAgeError("age is not a number")
                  }
                  if(age < 18){
                     throw new TooYoungError("user is younger than 18")
                  }
                  console.log(`You are ${age} years old`)
               }catch(err){
                  if(err instanceof InvalidAgeError || err instanceof SyntaxError){
                     // handle bad json inputs
                     console.log(`Ignoring invalid json input: ${err}`)
                  }else{
                     // let someone else deal with age issues
                     throw err
                  }
               }finally{
                  // undo loading state, do cleanup and enable button
                  button.disabled = false
                  button.innerText = "Submit"
                  input.value = ""
               }

            // catch re-thrown exception
            }catch(err){
               console.log(`Caught global exception ${err}`)
            }
         }
      </script>
   </body>
</html>

Now if a user enters bad data, we will just ignore it. If a user's age is too low, the finally block makes sure our cleanup code still runs before the exception is caught in the global scope. Finally free of soft-locks!

More articles

Resetting Windows passwords from linux

Regain control of your windows 7/8/10/11 pc with just a few commands

Understanding how LFI/RFI exploits work

Exploring the vulnerabilities in a demo application

Running minecraft server in a Docker container

A cross-platform way to run an isolated minecraft server with resource limits

Choosing the right RAID setup

Making sense of pros and cons for RAID configurations

A gentle introduction to systemd

A broad overview of systemd components and features

Advanced Docker Compose features

Getting more out of container stacks