David Y.
—I’m new to JavaScript development and would like to learn more about error handling.
I’ve seen try
…catch
statements in code, and I understand that they can be used to suppress error messages, but I don’t understand why one would do this rather than fixing the code so that the error doesn’t come up.
How does error handling work and when should I use it?
While it may be possible to fix most or all possible error conditions in very simple programs, this becomes increasingly difficult as code size and complexity increase.
Programs that rely on external inputs, such as user input or data from an API, can encounter error states due to failures in those external sources.
For example, external inputs could be flawed due to:
Error handling with try
…catch
allows developers to build execution flows that handle these unavoidable errors gracefully, without crashing the program.
Let’s consider a simple example. The below code asks the user to enter a number for it to double:
function doubleInput() { const userInput = prompt("Please enter a number:"); // prompt the user for a number try { const number = parseInt(userInput); // attempt to parse user input as an integer if (isNaN(number)) { // unparseable input will be stored as NaN (not a number) throw new Error("Invalid number entered"); // throw an error – this will move execution to the catch block below } console.log("Double that number is:", number * 2); // show number*2 in the console } catch (error) { console.error("Error:", error.message); // print the thrown error's message in the console alert("Please enter a valid number!"); // show an error message to the user doubleInput(); // ask the user again } } doubleInput(); // ask the user for a number and double it
Running this code will produce a prompt asking for a number.
In the try
block, the code attempts to parse that input as an integer.
The code will continue to prompt the user for a valid number until one is entered.
Code in a try
block will run until a throw
statement is encountered. At this point, the remainder of the try
block will be abandoned, and execution will switch to the nearest catch
block. This will happen no matter how deeply nested the throw
statement is. The code below demonstrates this:
function printA() { const userInput = prompt("Please enter a number:"); // prompt the user for a number try { const number = parseInt(userInput); // attempt to parse user input as an integer if (isNaN(number)) { // unparseable input will be stored as NaN (not a number) throw new Error("Invalid number entered"); // throw an error – this will move execution to the catch block below } console.log("A".repeat(10 / number)); // print A (10/number) times } catch (error) { console.error("Error:", error.message); // print the thrown error's message in the console alert("Please enter a valid number!"); // show an error message to the user printA(); // ask the user again } } printA(); // ask the user for a number and use it to print one or more As
This code can fail in two ways:
a
, it will output the error message Invalid number entered
and prompt the user to enter a valid number.0
, it will output the error message Invalid count value: Infinity
and prompt the user to enter a valid number.In the first case, the code behaves as explained in the previous example. In the second case, the error arises from providing the value Infinity
to String.prototype.repeat()
, causing it to throw a RangeError
, which is a subclass of Error
.
The instanceof
operator can be added to the catch
block to determine which error has been thrown:
function printA() { const userInput = prompt("Please enter a number:"); // prompt the user for a number try { const number = parseInt(userInput); // attempt to parse user input as an integer if (isNaN(number)) { // unparseable input will be stored as NaN (not a number) throw new Error("Invalid number entered"); // throw an error – this will move execution to the catch block below } console.log("A".repeat(10 / number)); // print A (10/number) times } catch (error) { console.error("Error:", error.message); // print the thrown error's message in the console if (error instanceof RangeError) { alert("Zero is not allowed."); // show a specific error message to the user } else { alert("Please enter a valid number!"); // show a general error message to the user } printA(); // ask the user again } } printA(); // ask the user for a number and use it to print one or more As
Sometimes, try
…catch
is accompanied by a third block: finally
. This block specifies the code to run regardless of whether an error is thrown.
For example, the finally
block could be used to make the code prompt the user for another number regardless of whether it has succeeded or failed.
function printA() { const userInput = prompt("Please enter a number (or X to exit):"); // prompt the user for a number let timeout = 1000; if (userInput.toUpperCase() === "X") { // exit the function return; } try { const number = parseInt(userInput); // attempt to parse user input as an integer if (isNaN(number)) { // unparseable input will be stored as NaN (not a number) throw new Error("Invalid number entered"); // throw an error – this will move execution to the catch block below } console.log("A".repeat(10 / number)); // print A (10/number) times timeout = 2000; // wait 2 seconds before asking again } catch (error) { console.error("Error:", error.message); // print the thrown error's message in the console if (error instanceof RangeError) { alert("Zero is not allowed."); // show a specific error message to the user } else { alert("Please enter a valid number!"); // show a general error message to the user } timeout = 1000; // wait one second before asking again } finally { setTimeout(function () { printA(); // ask the user again after a delay }, timeout); } } printA(); // ask the user for a number and use it to print one or more As
In this example, the catch
block could even be omitted entirely, creating a try
…finally
flow:
function printA() { const userInput = prompt("Please enter a number (or X to exit):"); // prompt the user for a number let timeout = 1000; if (userInput.toUpperCase() === "X") { // exit the function return; } try { const number = parseInt(userInput); // attempt to parse user input as an integer if (isNaN(number)) { // unparseable input will be stored as NaN (not a number) throw new Error("Invalid number entered"); // throw an error – this will move execution to the finally block below } console.log("A".repeat(10 / number)); // print A (10/number) times timeout = 2000; // wait 2 seconds before asking again } finally { setTimeout(function () { printA(); // ask the user again after a delay }, timeout); } } printA(); // ask the user for a number and use it to print one or more As
As the code in the finally
block will execute regardless of whether an error is thrown in the try
block, the code produces substantially the same behavior as our previous examples, only without the user-friendly error messages.
Without try
…finally
, our the would crash as soon as an error was thrown, so this can be a useful way to recover from the failure of non-essential code paths.
Now that we know how error handling works, we can think more productively about when to use it.
Using try
…catch
…finally
allows us to significantly alter the execution flow of our code and to recover gracefully from what would otherwise be fatal errors. It can be tempting to pepper our code with try
…catch
blocks in the hopes of making it more robust, but this would not only make our code more complex, it could also make the debugging process more difficult.
Consider the following code:
function printA() { const userInput = prompt("Please enter a number (or X to exit):"); // prompt the user for a number let timeout = 1000; if (userInput.toUpperCase() === "X") { // exit the function return; } try { const number = parseInt(userInput); // attempt to parse user input as an integer if (isNaN(number)) { // unparseable input will be stored as NaN (not a number) throw new Error("Invalid number entered"); // throw an error – this will move execution to the catch block below } console.log("A".repeat(10 / number)); // print A (10/number) times timeout = 2000; // wait 2 seconds before asking again } catch (error) { alert("An error occurred, please try again.") timeout = 1000; // wait one second before asking again } finally { setTimeout(function () { printA(); // ask the user again after a delay }, timeout); } }
We know from the previous examples that this code has two possible failure states:
Error
thrown explicitly in the try
block.RangeError
thrown by repeat()
when a user provides the value Infinity
.However, if we didn’t know about the RangeError
failure state, the code above would make it almost impossible to discover because it doesn’t log the caught error.
In this way, overly simple catch
blocks can mask issues in code that would otherwise lead to clear errors, potentially causing more subtle bugs later on.
For this reason, it is best practice to:
Tasty treats for web developers brought to you by Sentry. Get tips and tricks from Wes Bos and Scott Tolinski.
SEE EPISODESConsidered “not bad” by 4 million developers and more than 100,000 organizations worldwide, Sentry provides code-level observability to many of the world’s best-known companies like Disney, Peloton, Cloudflare, Eventbrite, Slack, Supercell, and Rockstar Games. Each month we process billions of exceptions from the most popular products on the internet.
Here’s a quick look at how Sentry handles your personal information (PII).
×We collect PII about people browsing our website, users of the Sentry service, prospective customers, and people who otherwise interact with us.
What if my PII is included in data sent to Sentry by a Sentry customer (e.g., someone using Sentry to monitor their app)? In this case you have to contact the Sentry customer (e.g., the maker of the app). We do not control the data that is sent to us through the Sentry service for the purposes of application monitoring.
Am I included?We may disclose your PII to the following type of recipients:
You may have the following rights related to your PII:
If you have any questions or concerns about your privacy at Sentry, please email us at compliance@sentry.io.
If you are a California resident, see our Supplemental notice.