Error handling is a vast and distinct programming subject. So far, we've managed to escape it, but in the real world, where applications contain thousands, and tens and hundreds of thousands (if not millions) of lines of code, error handling affects many things: how easy it is to modify and extend the code, and whether the program behaves properly in different situations.
In this lesson, we'll look at exceptions. But before we explore new concepts, let's talk about errors in general.
In JavaScript, strings have a method called text.indexOf(str)
. It looks for a substring str
within text
text and returns the index of the beginning of this substring in the text. What happens if the substring is not found? Is this behavior a mistake? No. This is normal function behavior. There's nothing wrong with not finding a substring. Imagine any text editor and a search engine within it. Nothing being found is a regular phenomenon that lefts the program employable.
By the way, look in the documentation of this function, how does it tell you that the substring wasn't found?
Another situation. These editors have an "open file" function. Imagine if anything went wrong when you attempted to open the file like it was destroyed. Is this error or not? Yes, there was an error here, but it wasn't a programming error. No matter what the programmer intends, this kind of error is always possible. It is unavoidable. The programmer's sole option is to handle it correctly.
Another interesting question is, "how critical is this error?” Should it cause the entire application to stop? In poorly written applications, where error handling is flawed, this situation will cause the whole application to crash and terminate. In a well-written application, nothing wrong will happen. The user will see a warning that the file is unreadable and will be able to choose further actions, such as trying to read it again or performing another action.
The preceding has serious consequences. At different levels, this event may either represent an error or be completely normal. For instance, there will be an error from the standpoint of a function if it is supposed to read a file but fails to do so. Should the entire application be terminated as a result? It should not, as we discovered above. It's up to the application that uses the feature to decide how critical the situation is, but not the feature itself.
Return codes
In languages that appeared before 1990 (approximately), error handling was done through a mechanism wherein a function returns a special value. For example, in C, if a function fails, it must return a special value, either NULL
or a negative number. The value of this number tells you what kind of error occurred. For example:
int write_log()
{
int ret = 0; // return value 0 if success
FILE *f = fopen("logfile.txt", "w+");
// Checking if the file can be opened
if (!f)
return -1;
// Checking that we haven't reached the end of the file
if (fputs("hello logfile!", f) != EOF) {
// continue using the file resource
} else {
// File has ended
ret = -2;
}
// Failed to close the file
if (fclose(f) == EOF)
ret = -3;
return ret;
}
Note the conditional constructs and the constant assignment by the ret
variable. Every highly unsafe procedure needs to be successfully tested. The function returns a unique code if something goes wrong.
And this is where the problems begin. As life shows, in most situations, the error is not handled where it occurred, nor even at a level higher. Consider a function called A that calls code throwing an error, and it should be able to handle it correctly and notify the user. The error itself occurs inside function E, which is called inside A indirectly through a chain of functions: A => B => C => D => E. Think about what this might lead to. All functions in this chain must be aware of the error, catch it, and return the error code even though they don't handle it. As a result, there's so much code for dealing with bugs that the code doing the original task falls by the wayside.
It's important to note that there are error-handling techniques that follow the returning principle but don't suffer from these drawbacks. For example, the Either Monad.
Exceptions
It was in this context that the exception method arose. Its main purpose is to pass the error from where it originated to where it can be processed, bypassing all the intermediate levels. In other words, the exception mechanism unwinds the call stack by itself.
There are two things to remember about exceptions: the code where the error occurred throws an exception, and the code where the error is handled - where it's caught.
// A function that can throw an exception
const readFile = (filepath) => {
if (!isFileReadable(filepath)) {
// throw is a way to throw an exception
throw new Error(`'${filepath}' is not readable`);
}
// ...
}
// Somewhere else in the program
const run = (filepath) => {
try {
// The function that calls readFile. Perhaps not directly, but through other functions.
// It's not important for the exception mechanism.
readFile(filepath);
} catch (e) {
// This block is only executed if an exception is thrown in the try block
showErrorToUser(e);
}
// If there's code here, it'll continue to run
}
The exceptions themselves are Error
objects. These objects contain the message passed to the constructor, the stack trace, and other useful data inside.
The exception is thrown by itself with the throw
keyword::
const e = new Error('Any text here');
throw e; // The exception can be created separately, or it can be created straight away when throw is used
throw
stops the code from being executed any further. In this sense, it's similar to return
, but unlike return , it interrupts not only the current function, but the entire code, up to the nearest catch
block in the call stack.
The try/catch block is usually placed at the top level of the program, but this isn't crucial. It's likely that there are several intermediate blocks that can catch errors and re-initiate them. This topic is quite complex and requires a certain amount of experience.
Are there any more questions? Ask them in the Discussion section.
The Hexlet support team or other students will answer you.
For full access to the course you need a professional subscription.
A professional subscription will give you full access to all Hexlet courses, projects and lifetime access to the theory of lessons learned. You can cancel your subscription at any time.