Skip to content

Handling errors

Programs fail on input that doesn't fit, on servers that don't answer and on data that looks different than expected. In part 1 and part 7 you load data from a server and have to deal with such errors there. This unit covers the basics.

Learning goals

After this unit you can

  • raise an error with throw and give it a clear message,
  • catch errors with try and catch and explain when finally runs,
  • recognize the most common error types of JavaScript,
  • write an error class of your own and handle errors selectively.

throw raises an error. The program stops at that point and jumps to the nearest matching catch. If there is none, the whole program ends with the error's message.

parse.mjs
function parseDeposit(text) {
const value = Number(text)
if (Number.isNaN(value) || value < 0) {
throw new RangeError(`Invalid deposit ${text}`)
}
return value
}
for (const text of ['20', '-5', 'twenty']) {
try {
console.log(parseDeposit(text))
} catch (error) {
console.log(error.name, error.message)
} finally {
console.log('checked', text)
}
}

parseDeposit throws a RangeError when the text isn't a valid number or the number would be negative. The loop calls the function in a try block. With '20' everything runs normally. With '-5' and 'twenty' the program jumps to the catch block, which prints the name and message of the error. The finally block runs in every case, with or without an error. It is the place for cleaning up, for example hiding a loading indicator.

JavaScript has several built-in error types. The language throws most of them itself.

TypeWhen it occursExample
TypeErrora value doesn't have the expected typeundefined.name
ReferenceErrora variable doesn't exista typo in the name of a variable
SyntaxErrorcode or JSON isn't validJSON.parse("{ 'name': 'Tent' }")
RangeErrora value is outside the allowed range(20).toFixed(200)
Errorgeneral error for your own casesthrow new Error('Server unreachable')

Every error has a field name with its type and a field message with its message. In the console you also see the stack, the chain of calls up to the place where the error occurred.

For errors that belong to your own app, a class of their own pays off. It extends Error and can carry extra fields.

custom.mjs
class NotAvailableError extends Error {
constructor(item) {
super(`${item} is on loan`)
this.name = 'NotAvailableError'
this.item = item
}
}
function reserve(item, available) {
if (!available) throw new NotAvailableError(item)
return `${item} reserved`
}
try {
console.log(reserve('Drill', true))
console.log(reserve('Tent', false))
} catch (error) {
if (error instanceof NotAvailableError) console.log(error.message)
else throw error
}

NotAvailableError calls the constructor of Error with super, sets its own name and remembers the thing. The catch block checks with instanceof whether it is this error. It rethrows other errors with throw error, because at this point the program doesn't know how to deal with them. Reserving the drill works, and for the tent the program jumps to the catch block.

MistakeWhat helps
An empty catch block swallows every error, and nobody notices anything.At least print the error with console.error or rethrow it.
catch treats all errors the same, including programming errors such as a TypeError.Check with instanceof and rethrow unknown errors.
throw 'Invalid' throws a string instead of an error, and error.message is undefined.Always throw an error object, throw new Error('Invalid').
Code after a return in the try block is meant to clean up and never runs.Put the cleanup in finally, which also runs after a return.
An AI assistant wraps every call in try and catch and only prints "Error".Handle errors where you can do something useful, and show a message that says what happened.

Exercise Getting started

Write a function parseDays(text) that reads a number of days from a form field. Whole numbers from 1 to 7 are allowed, otherwise the function throws a RangeError with a message that says what is allowed. Test with '3', '9' and '2.5' and print the message when there is an error.

Solution
solution-1.mjs
function parseDays(text) {
const days = Number(text)
if (!Number.isInteger(days) || days < 1 || days > 7) {
throw new RangeError(`A loan lasts 1 to 7 days, not ${text}`)
}
return days
}
for (const text of ['3', '9', '2.5']) {
try {
console.log(parseDays(text))
} catch (error) {
console.log(error.message)
}
}

Number.isInteger rejects 2.5 and also NaN, in case someone types text. The message names the allowed range and the wrong value, so the member knows what to change.

Exercise Going further

The app's settings are stored as JSON text. Write a function readSettings(text) that reads the text with JSON.parse. If the text isn't valid JSON, it prints a message to the console and returns the default settings { pageSize: 20 }.

Solution
solution-2.mjs
function readSettings(text) {
try {
return JSON.parse(text)
} catch (error) {
console.log('Settings unreadable, using the default', error.name)
return { pageSize: 20 }
}
}
console.log(readSettings('{"pageSize": 50}').pageSize)
console.log(readSettings('pageSize: 50').pageSize)

The output is 50, then the message with SyntaxError, and then 20. The second text isn't JSON, because the key would have to be in double quotes and the whole thing in curly braces. The return in the catch block makes sure the app keeps running with usable values.

Exercise Challenge

Does finally run even when the catch block leaves the function with return? Write a function load that throws an error, catches it and returns 'fallback' in the catch block. Record in an array which blocks run in which order.

Solution
solution-3.mjs
const steps = []
function load() {
try {
steps.push('try')
throw new Error('Server unreachable')
} catch (error) {
steps.push('catch')
return 'fallback'
} finally {
steps.push('finally')
}
}
console.log(load(), steps.join(' > '))

The output is fallback try > catch > finally. finally runs after catch has determined the return value, but before the function actually returns. That's why finally is the right place for cleaning up, no matter which way a function ends.

Text and figures are licensed under CC BY-SA 4.0, the code under the MIT License and the course data under CC0 1.0. The figures use the font Kalam under the SIL Open Font License 1.1. The sources are on GitHub.