Clean Code Applied to JavaScript - Part V. Exceptions
This post is the fifth of an interesting series of posts that will delve into the well-known topic that is "Clean Code" but applied to JavaScript.
In this series, we are going to discuss the classic tips around clean code that every programmer should know but applied to a specific JavaScript/TypeScript language.
- Part I. Before your start.
- Part II. Variables.
- Part III. Functions
- Part IV. Comments
- Part V. Exceptions
Introduction
The exceptions are an essential piece in the development of quality software since we will be controlling unexpected or non-implemented situations. Therefore, developers sometimes confuse error handling with software flow treatment. Exceptions should be used to deal with uncontrolled or developed situations in our software and never as a way to simulate a "return" of our business logic to derive the flow of software in one direction or another.
In this post, we will provide some advice related to handling exceptions that will allow your code to remain clean using exceptions
Prefer Exceptions to Returning Error Codes
Use exceptions better than error codes when the programming language has exception handling. This statement seems obvious, but it is not so since many programmers learned with a programming language lacking this feature or have not seen the potential it has and have omitted its use. However, using exceptions will produce a much cleaner code than having to manage error codes in the code itself.
The following code shows a class in which no exceptions are being used and the management of uncontrolled cases must be carried out manually, through the "if" statement. Instead, we have to delegate all this tedious and dirty task to language through exceptions. Observe the second code in which business logic has been separated with error management. The code has the following advantages:
- Uncoupled business logic and error control. They are two different problems to solve and must be separated and treated differently.
- Less verbosity in the code and easier to read.
- The responsibility for the error code has been delegated to the programming language, which must be at our service and not vice versa.
// Dirty
class Laptop {
sendShutDown() {
const deviceID = getID(DEVICE_LAPTOP);
if (deviceID !== DEVICE_STATUS.INVALID) {
const laptop = DB.findOne(deviceID);
if (laptop.getStatus() !== DEVICE_SUSPENDED) {
pauseDevice(deviceID);
clearDeviceWorkQueue(deviceID);
closeDevice(deviceID);
} else {
logger.log('Device suspended. Unable to shut down');
}
} else {
logger.log('Invalid handle for: ' + DEVICE_LAPTOP.toString());
}
}
}
// Clean
/*
The code is better because the algorithm
and error handling, are now separated.
*/
class Laptop {
sendShutDown() {
try {
tryToShutDown();
} catch (error) {
logger.log(error);
}
}
tryToShutDown() {
const deviceID = getID(DEVICE_LAPTOP);
const laptop = DB.findOne(deviceID);
pauseDevice(deviceID);
clearDeviceWorkQueue(deviceID);
closeDevice(deviceID);
}
getID(deviceID) {
throw new DeviceShutDownError('Invalid handle for: ' + deviceID.toString());
}
}
Don't ignore caught error!
Please do not do the ostrich technique!
The ostrich technique consists of hiding the head under the earth and that is what we do every time we have an error management where we do absolutely nothing.
It is very important that you learn that doing a console.log, or system.out.println about an error means NOT doing anything. In fact, it is more dangerous because in case we were doing this false control when the exception occurred we would see it appear. Therefore, do not ignore the management of an exception, the exceptions are caused by an unexpected circumstance and must be treated properly.
In the first code, it is the usual treatment of junior programmers or programmers who apply the ostrich technique, something quite easy since the error has stopped interrupting the application but what really should be done is the second example, in which we make a correct treatment. Of course, I know that doing an error treatment takes time and effort.
try {
functionThatMightThrow();
} catch (error) {
console.log(error);
}
try {
functionThatMightThrow();
} catch (error){
console.error(error);
notifyUserOfError(error);
reportErrorToService(error);
}
Don't ignore rejected promises
As in the previous case when we ignore the treatment of an error. In JavaScript we have asynchronism and one of the tools we have to deal with asynchronism are promises.
The promises can be rejected (not that it is a mistake itself) and therefore we have to manage them as if they were mistakes.
In this case, we see the same example as the previous case but applied on promises.
getData()
.then(data => functionThatMightThrow(data))
.catch(error => console.log);
getData()
.then(data => functionThatMightThrow(data))
.catch(error => {
console.log(error);
notifyUserOfError(error);
reportErrorToService(error);
});
Exceptions Hierarchy
Create a hierarchy of exceptions. Every programming language has a set of its own low-level exceptions: NullPointerException or
ArrayIndexOutOfBoundsException. These exceptions do not talk about our business logic, they do not give us anything. It makes no sense to use those exceptions to control the errors that occur in our code since our code is modeling a business logic. Therefore, we have to create our own hierarchy of exceptions that speaks of our business logic and that triggers when an unexpected situation occurs in our business logic.
In the following example, two exceptions have been created, which are called UserException and AdminException, these exceptions occur on two types of users but no longer occur on a data structure. Now we have business logic, in fact, these two exceptions are too generic and we could define exceptions of the type: UserRepeatException, UserNotFoundException, etc ...
We have a contribution of semantic value of our exceptions there that we would not otherwise obtain.
export class UserException extends Error {
constructor(message) {
super(`User: ${mesage}`);
}
}
export class AdminException extends Error {
constructor(message) {
super(`Admin: ${message}`);
}
}
// Client code
const id = 1;
const user = this.users.find({ id });
if(user){
throw new UserException('This user already exists');
}
Provide context with exceptions
Although the exceptions have a stack trace that allows us to see the chain calls at the time that an exception has occurred this is complicated to understand. Therefore, add context to the exceptions to improve this feature. Normally, a message is added explaining the intention of the operation that failed in our software. Please do not use an indecipherable code for humanity. It should be noted that this information we provide should not be what the end user sees since we should properly manage the exception for that so these codes are not shown in the user interface but something more usable for them.
If we develop a hierarchy of exceptions we will have provided context to the exceptions.
Conclusions
In this post, we have presented some recommendations for creating exceptions.
The exceptions are a fundamental piece in the development of quality software and in many occasions they are ignored or simply tried to keep incorrect to redirect the flow of the application.
In any case, if the programming language provides this feature we must take advantage of it and delegate it to the languages to focus on the business logic.
Finally, the points we have addressed are the following:
- Prefer Exceptions to Returning Error Codes
- Don't ignore caught error!
- Don't ignore rejected promises
- Exceptions Hierarchy
- Provide context with exceptions