Errors & Debugging
Errors
Errors are something that developers will experience throughout their careers. They can, especially as you're learning to become a developer, seem confusing and overwhelming but they are nothing to be feared.
The aim is not to become a sufficiently good developer that you never encounter errors - this is an unachievable goal. The aim is to learn how to approach fixing the errors when they do inevitably occur.
Reading Error Outputs
The following snippet of code contains an error.
function sumUpTo(num) {
let total = 0;
for (let i = 1; i <= num; i++) {
total += i;
}
return totol;
}
sumUpTo(5);If we try to run this code we might see something like this in the console.
/Users/devups/repos/errors-debugging/main.js:6
return totol;
^
ReferenceError: totol is not defined
at sumUpTo (/Users/devups/repos/errors-debugging/main.js:6:3)
at Object.<anonymous> (/Users/devups/repos/errors-debugging/main.js:25:1)
at Module._compile (node:internal/modules/cjs/loader:1120:14)
at Module._extensions..js (node:internal/modules/cjs/loader:1174:10)
at Module.load (node:internal/modules/cjs/loader:998:32)
at Module._load (node:internal/modules/cjs/loader:839:12)
at Function.executeUserEntryPoint [as runMain] (node:internal/modules/run_main:81:12)
at node:internal/main/run_main_module:17:47Our first step to being able to deal with errors is recognising that there is a lot of information that we can make use of here. We don't need to understand every single word in this output - we just need to identify the parts that can help us.
Reading this error output from top to bottom, we can see that it tells us:
- The file path - the file in which the error occurred
- The line number
- Location - a small arrow pointing to the part of the code where the error occurred
- Type of error - in this case it's a
ReferenceError - Error description - elaboration on what caused the error, here it's
totol is not defined - The stack trace - the block of information below the error type
Error types
There are lots of different kinds of errors in JavaScript. MDN provides us with both a list of the different error types (opens in a new tab) and a comprehensive breakdown of specific examples of those errors (opens in a new tab). The main three kinds of errors you will see in JavaScript are:
SyntaxError
This happens when you break the rules of JavaScript syntax. We can think of this as being equivalent to breaking the rules of grammar and punctuation in the English language. In English, we might break the rules by not ending a sentence with a punctuation mark; in JavaScript, we might write an opening bracket without a corresponding closing bracket.
Syntax errors are the first kinds of errors that show up - before executing the code, Node will run a process called tokenization during which it checks if your code is syntactically valid. If it isn't, then Node is incapable of executing it as JavaScript.
ReferenceError
A reference error occurs when we try to access a variable that we are unable to access at that point. It might be that the variable doesn't exist, hasn't been created yet in your program, or is outside of scope (we will be discussing this last point in a lecture next week).
Reference errors are an example of a "runtime error", meaning that they occur whilst the code is running (being executed). Any runtime error will stop the execution of the program - any code written after the point where the error happens will not be executed.
TypeError
A type error is the result of treating one JavaScript type as if it is another. For example, if you try to invoke a number as if it is a function, this will result in a type error.
Type errors are another example of a runtime error.
Note: there are instances where you might accidentally try to use one data type as if it is another and JavaScript will not throw an error. It will instead perform type coercion (opens in a new tab) on the value.
The Stack Trace
The stack trace takes up most of what forms the error output. It shows us which functions, in order, are in the process of being executed at the point where the error occurred. It is a reflection of the callstack at that moment.
A significant chunk of the output is functions that are being used behind-the-scenes when Node is executing our file. However, the first few lines of the stack trace can be useful to us when our program extends across multiple files.
For example, you might see an output such as this.
at convertNumToString (/Users/devups/repos/errors-debugging/file-1.js:1:39)
at getLastDigit (/Users/devups/repos/errors-debugging/file-2.js:4:21)
at Object.<anonymous> (/Users/devups/repos/errors-debugging/main.js:18:1)By looking at each line in turn, we can learn:
- There is an error being caused in the
convertNumToStringfunction on line 1 offile-1.js - This function is actually being called from a different file -
file-2.js, on line 4 from inside thegetLastDigitfunction - Lastly, the
getLastDigitfunction is being called on line 18 of themain.jsfile
This serves as a trail that we can follow to find the source of the problem. The error is being thrown on line 1 of file-1.js but the problem might be caused elsewhere. The stack trace is telling us where to look.
Debugging with console.log
The key to debugging code is to identify where the ideas of "what we expected to happen" and "what actually happened" diverged. Our code will contain many variables whose values we cannot be certain of without inspecting them. console.log is one such way to take a closer look at what their value actually is.
Following the way that JavaScript executes our programs - line by line, top to bottom (and using the stack trace as a guide) - we can console.log each variable in turn, taking note to repeatedly ask the questions:
- What value did I expect this variable to have?
- What value does it actually have?
If the answer to the second is different to the answer to the first, then we have significantly narrowed down where the source of the problem might be. Then, we can start to ask more specific questions like
- What value did this variable have when it was initially created?
- What logical operations led to it having this value?
- Has its value been changed since it was created?
Continuing the process of logging more values within our program, we can continue to reduce the amount of code where the problem might have arisen from.
Ultimately, logging cannot fix the problem - it is up to us to do that. But it can be a hugely significant aid in the process of identifying where the problem is and what went wrong along the way.
Not All Problems Cause Errors
Sometimes, our code just doesn't work. It doesn't necessarily throw an error, but the output isn't what it should be. In this situation, we cannot take advantage of any error output in the console to help guide us through the process of solving it, but that doesn't mean we can't utilise the same process of logging that is described above.
You should continue to follow the execution of your program - across different functions and files if necessary - using console.log to observe the values at each stage, and ask yourself the same questions that were discussed above.
Summary
Errors are not something we can avoid, but they are things that we can work to fix by taking a thoughtful approach.
The key points to remember are:
- Use any error output in the console to maximum benefit - there is a ton of information in our hands already, we just need to read it carefully.
- Use
console.logto compare what you expect a value to be to what it actually is. Do this slowly, one value at a time, to narrow down the region where the issue might be.