Error Handling in Javascript

Errors in Javascript have two parts: a throw and a try... catch statement.

throw creates an exception which will 'bubble up' through scopes until it either reaches a try... catch block or crashes your program. Exceptions can be of any type:

throw "An error"
throw 500
throw false
throw new Error("This is an error")

The try... catch block will capture an error from an inner scope. The finally block runs after the try...catch block regardless of if the error was triggered. One common usage of finally is to handle default behaviour. For example, shutting down connections or closing files are often handled in finally blocks, because they should always happen regardless of the success or failure of the operation.

function alwaysError() {
  throw true
}

try {
  alwaysError()
}
catch (e) {
  // console.error is helpful for logging out errors
  console.error(e)
}
finally {
  console.log("Everything's under control.")
}

One Common Mistake

A common mistake for beginner programmers is to write try...catch blocks to handle every possible error case. This is usually not the best way to deal with errors.

The example below shows an example of what you often shouldn't do. Can you see the problem?

function raiseToPower(num, pow) {
  if (typeof num !== "number" || typeof pow !== "number") {
    throw new Error("Number or power are not numbers.")
  }
  let result = num;
  for (let i = pow; i > 0; i--) {
    result = result * num
  }
  return result
}

let pow

try {
  pow = raiseToPower("hello")
}
catch (e) {
  console.error(e)
}

console.log(pow)

Instead of failing out of the process, we continue as if we successfully processed the user error. This means that instead of failing immediately, we set ourselves up for a later (and harder to debug) failure later in the program.

This example is a better example of where using a try... catch block makes sense.

function readAFile(filePath) {
  try {
    openFile(filePath)
    let fileContents = readFile(filePath)
    closeFile(filePath)
    return fileContents
  }
  catch {
    throw new Error("File couldn't be opened.")
  }
}

const paths = ["./file1","./file2","./file3"]

let myFile

for (let path of paths) {
  try {
    myFile = readAFile(path)
  }
}

In this example we try to read each path in a list of files. If it fails to read one path, we throw an exception, but we don't really mind too much.

try...catch blocks are best used when we want silent failures or managed errors, and therefore aren't a good choice for catching unanticipated bugs. They're a much better fit for predictable bugs that might happen due to factors outside our program, like expected files not existing.

Exercises

To read a file in Node.js we can use fs.readFile:

const fs = require("node:fs")
const prompt = require("prompt-sync")()

const data = fs.readFileSync("path", "utf8")

Write a program which takes a space-separated list of files from the user and prints out the word count of each file. A word is a set of characters separated by whitespace.

If the program is unable to read any files, it should print out the files it's unable to read and continue adding to the wordcount.

For text you can use the standard Lorem Ipsum passage:

Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.