loading...
Cover image for Refactoring node.js (Part 2)

Refactoring node.js (Part 2)

paulasantamaria profile image Paula Santamaría Updated on ・7 min read

Welcome to the second part of "Refactoring node.js". In this series, I'm sharing some tips and tools that I believe can help you write more effective and cleaner Node.js code.

Contents

1. Fail early with strict mode

I found out about strict mode while reading the code for mongoose in Github. I was really curious about it, why does every file in the whole library begin with 'use strict'?

ECMAScript 5 introduced strict mode in Javascript. When we apply strict mode it's easier to write cleaner and more secure code because Javascript becomes less permissive:

  • We can't get away with things like undeclared variables:
'use strict' 

undeclaredVar = 10; // Throws an error.
  • Variables with names that match Javascript keywords like let are not allowed:
'use strict' 

var let = 10; // Throws an error.
  • All parameters in a function must have unique names:
'use strict' 

// Throws an error.
function sum (a, a) {

}

How do we use strict mode in our code?

We can "activate" strict mode by simply writing use strict in any Javascript file

'use strict' 

// JS Code.

Or, if we want, we can use it in specific functions:

function strictFunction() {
    'use strict' 
    // Function code.
}

function notStrictFunction() {
    // Function code.
}

Interesting fact: Javascript modules are strict by default, so we don't need to apply strict mode explicitly.

module.exports.noNeedForUseStrict = () => {
    // Strict mode by default.
}

Why use strict mode?

By not being able to get away with the things I mentioned before (and many more), it becomes easier to write more secure code that has a lower probability to fail. We're immediately alerted when we write insecure code, and failing early prevents our small mistakes to get committed, which also helps us learn and apply better practices.

More info about strict mode: MDN - Strict mode.

2. Use a linting tool

One of the most annoying issues I used to have when writing Javascript is that I was always switching between single and double-quotes for strings. But then I read about ESLint somewhere, and after a bit of setup, I could make sure that all my strings used single-quotes.

Using a linting tool like ESLint will help you:

  • Setup style rules for you and your team to follow, which keeps the code standardized and easy to read for anyone on the team.
  • Discover errors like unused variables
  • Easily follow predefined style guides that will keep your code easier to read for more than just you and your team. E.g: Google JavaScript style guide.

Installing ESLint

  1. Execute npm install eslint --save-dev on your project
  2. The CLI will help you set up the basic config

You can also customize the default config by editing the file .eslint.js.

Running ESLint

After you've setup your style rules all that's left to do is run ESLint and start fixing any issue that it might find.

To run ESLint in your whole project: eslint ./

This command will scan your code and let you know if one of the rules you setup is not followed somewhere. It will also provide you with an error code, the line where the error is and a quick description of the issue:

const myConst = 10;
myConst = 11;

// ESLint result:
// 2:1 error  'myConst' is constant                         no-const-assign

If the description is not good enough, you can always use the error code to check an in-depth explanation of the issue using the search field in ESLint site.

But wait, there's more!

You can apply automatic fixes using the --fix option. This will not solve all the issues, as some of them may require human intervention, but it'll solve a lot of them which makes refactoring process a lot easier.

I also wrote about running ESlint in your CI pipeline.

3. Write JSDoc documentation

JSDoc is an open-source API documentation generator for Javascript. It allows developers to document their code through comments.

Here's how a function documented with JSDoc looks:

/**
 * Retrieves a user by email.
 * @async
 * @param {String} email - User email
 * @returns {User} User object
 * @throws {NotFoundError} When the user is not found.
 */
const getByEmail = async (email) => {
    // ...
}

What is so good about JSDoc?

  • You can document your code as thoroughly as you like with a huge list of tags like @param.
  • After documenting your code you can export the docs into an HTML website with a simple command: jsdoc r
  • It has built-in support in VSCode
  • The exported website's layout can be customized and there are templates available on Github.
  • Even if you don't export it, the documentation is helpful for anyone reading your code (especially if you use VSCode or any IDE that supports JSDoc).

If you want to learn more about JSDoc, I wrote an article entirely on that subject: Document your Javascript code with JSDoc

4. Use async FS methods with fs.promise

Last time I wrote about util.promisify and how to use it to convert fs callback modules to promises. But later in the comments @hugo__df pointed out something:



<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN" "http://www.w3.org/TR/REC-html40/loose.dtd">

Node 10+ has a promisified version of fs under fs.promises.

const fs = require ('fs').promises

So ever since Node.js v10 we can use fs with promises like so:

const fs = require('fs').promises;

const readFile = async (path) => {
    // Check if the path exists.
    const stats = await fs.stat(path);

    // Check if the path belongs to a file.
    if (!stats.isFile())
        throw new Error('The path does not belong to a file');

    // Read file.
    return await fs.readFile(path);
}

No need for util.promisify in fs operations anymore!

5. Gotta catch 'em all: Using a global error handler

In my last post, I wrote about using descriptive error Types to make our errors easier to identify and handle, and some of you asked me to write more about error handling, so let's talk about global error handling.

What is global error handling?

Global error handling allows us to catch all errors, in a single place. It's the last barrier to catch an error and decide what to do with it.

The advantages of having a global error handler are many:

  • We can define standard rules to handle errors that will always be applied.
  • The user or client will never receive a completely unhandled error.
  • All errors can be logged somewhere and even notify the team when they occur.
  • It makes our software secure by making sure we won't send any private info (like table names) to the client.
  • Dev environments require more info about the error that productive environments. We can make sure our software lets the devs know the details of the error, without exposing all the info to users or clients.
  • The client or user need clear information about the error to act accordingly. A good global error handler implementation will make sure of that.
  • We can actually decide what we want to do with an error that might otherwise be exposed to anyone.

How do we implement it?

The implementation of a global error handler varies depending on the type of software we're writing, especially since Node.js can be used to build REST APIs, CLIs, jobs, etc.

If you're using a Framework you'll probably find that it already contemplates the definition of a global error handler, so I encourage you to check the docs.

Let's look at Express.js for example.

Error handling in Express.js

Express.js is a web framework for Node.js, and it has it's own error handling strategy that you can take advantage of.

To handle all errors with express we need to use a middleware. A simple middleware will look like this:


const express = require('express');
const app = express();

app.get('/', (req, res) => {
    res.send('Hello world!');
});

// Define the error handler after other middleware and endpoints.
app.use(errorHandler)

app.listen(port);

/**
 * Error handling middleware.
 */
function errorHandler(err, req, res, next) {
    logError(err);

    if (err.statusCode) {
        // All errors that have a status code are safe.
        res.status(err.statusCode).send({ error: err.message });
    }
    else {
        res.status(500).send({ error: 'Something went wrong' });
    }
}

As you can see, what we're doing here is catch any errors that might happen and defining the following very simple rules:

  • All errors are logged somewhere.
  • If our error has a statusCode, we assume it's safe to return it to the client with the same status code and message that's defined in the error.
  • If our error doesn't have a statusCode, we return a generic message with status code 500 (internal server error).

Even in this very simple example, we're already making sure that no unhandled errors are returned to the client, all errors are logged so we can later evaluate if something needs to be fixed and the client receives enough information to act accordingly. Our software is secure and the client happy.

Error handling tools

In the previous example, we wrote a custom global error handler that logs the error somewhere. That might be enough to handle errors at an early stage of the project, but we'll probably need more than that eventually. For example, it'd be nice the get notifications and reports about the errors thrown by our app so we can act fast to fix them.

Short story: When @maurogarcia_19 and I launched Cephhi, our latest project, we had an awful bug that only presented itself on non-English browsers. The error was being logged, but we find out about it from our users first. If we had received an automatic notification we could've fixed the error faster, reducing the number of users affected. Lesson learned!

There are quite a few tools for error monitoring and reporting. I'm currently trying Bugsnag. What I like about it so far is that:

  • It's really easy to integrate with JavaScript
  • It has a free plan that works for small businesses
  • It can be integrated with Slack, so you get a message and a set of "actions" to execute every time an error is thrown.

Have you tried any other tools? Let me know!

Thoughts? 💬

Were these tips useful?

Would you like me to write about any other node.js related topics on the next article of the series?

What are your tips to write effective/clean node.js code?

I'll like to hear your feedback!

Discussion

pic
Editor guide
Collapse
singh1114 profile image
Ranvir Singh

For error handling, I have seen a lot of people using a combination of sentry and datadog

Collapse
paulasantamaria profile image
Paula Santamaría Author

I haven't heard about datadog. Will check it out, thanks!

Collapse
codenutt profile image
Jared

Definitely love Sentry

Collapse
codenutt profile image
Jared

Great article! Having a global error handler makes things way easier.

Collapse
paulasantamaria profile image
Paula Santamaría Author

Thank you, Jared! I agree. Error handling is one of the most important and sometimes underestimated features.

Collapse
pavelloz profile image
Paweł Kowalski

I like the idea of documentation generated from code, but i hate seeing so much comments in my code. JSDoc is so verbose, 50%+ of the file would be comments.

Collapse
paulasantamaria profile image
Paula Santamaría Author

To be honest I used to feel that way too, but I got used to it.
I'll be happy to include any alternatives in the article. Do you know any you'd recommend?