Your Web News in One Place

Help Webnuz

Referal links:

Sign up for GreenGeeks web hosting
November 9, 2019 11:23 pm GMT

Refactoring node.js (Part 1)

This is the first part of a series of articles where I'll share tips to write cleaner and more effective node.js code.

1. Use async/await

So there are 3 ways of writing asynchronous code in Javascript: callbacks, promises and async/await.

(If you haven't escaped callback hell yet, I encourage you to check out another dev.to article: How to Escape Callback Hell with JavaScipt Promises by @amberjones)

Async/await allows us to build asynchronous non-blocking code with a cleaner and more readable syntax than promises .

Let's see an example, the following code executes myFuncion(), returns the result and handles any errors that may be thrown by the function:

// PromisesmyFunction()    .then(data => {        doStuff(data);    })    .catch(err => {        handle(err);    });
// async/awaittry {    const data = await myFunction();    doStuff(data);}catch (err) {    handle(err);}

Isn't it cleaner and easier to read with async/await?

A few of extra tips regarding async/await:

  • Any function that returns a Promise can be awaited.
  • The await keyword can only be used within async functions.
  • You can execute async functions in parallel using await Promise.all([asyncFunction1, asyncFunction2]).

2. Avoid await in loops

Since async/await is so clean and readable we may be tempted to do something like this:

const productsToUpdate = await productModel.find({ outdated: true });for (const key in productsToUpdate) {    const product = productsToUpdate[key];    product.outdated = false;    await product.save();}

The above code retrieves a list of products using find and then iterates through them and updates them one by one. It will probably work, but we should be able to do better . Consider the following alternatives:

Option A: Write a single query

We could easily write a query that finds the products and updates them all in one, thus delegating the responsibility to the database and reducing N operations to just 1. Here's how:

await productModel.update({ outdated: true }, {    $set: {        outdated: false    } });

Option B: Promise.all

To be clear, in this example the Option A would definitely be the way to go, but in case the async operations cannot be merged into one (maybe they're not database operations, but requests to an external REST API instead), you should consider running all the operations in parallel using Promise.all:

const firstOperation = myAsyncFunction();const secondOperation = myAsyncFunction2('test');const thirdOperation = myAsyncFunction3(5);await Promise.all([ firstOperation, secondOperation, thirdOperation ]);

This approach will execute all the async functions and wait until all of them have resolved. It only works if the operations have no dependencies with one another.

3. Use async fs modules

Node's fs module allows us to interact with the file system. Every operation in the fs module contains a synchronous and an asynchronous option.

Here's an example of async and sync code to read a file

// Asyncfs.readFile(path, (err, data) => {    if (err)        throw err;    callback(data);});// Sync return fs.readFileSync(path);

The synchronous option (usually ends with Sync, like readFileSync) looks cleaner, because it doesn't require a callback, but it could actually harm your application performance. Why? Because Sync operations are blocking, so while the app is reading a file synchronously its blocking the execution of any other code.

However, it will be nice to find a way we could use the fs module asynchronously and avoid callbacks too, right? Checkout the next tip to find out how.

4. Convert callbacks to promises with util.promisify

promisify is a function from the node.js util module. It takes a function that follows the standard callback structure and transforms it to a promise. This also allows to use await on callback-style functions.

Let's see an example. The function readFile, from node's fs module, follows the callback-style structure, so we'll promisify it to use it in an async function with await.

Here's the callback version:

const util = require('util');const fs = require('fs');const readFile = (path, callback) => {    fs.readFile(path, (err, data) => {        if (err)            throw err;        callback(data);    });}

And here's the "promisified" + async version :

const util = require('util');const fs = require('fs');const readFile = util.promisify(fs.readFile);const readFile = async (path) => {    return await readFile(path);}

5. Use descriptive Error types

Let's say we're building an endpoint for a REST API that returns a product by id. A service will handle the logic and the controller will handle the request, call the service and build the response:

/* --- product.service.js --- */const getById = async (id) => {    const product = await productModel.findById(id);    if (!product)        throw new Error('Product not found');    return product;}/* --- product.controller.js --- */const getById = async (req, res) => {    try {        const product = await productService.getById(req.params.id);        return product;    }    catch (err) {        res.status(500).json({ error: err.message });    }}

So, what's the problem here? Imagine that the first line of our service (productModel.findById(id)) throws a database or network related error, in the previous code the error will be handled exactly the same as a "not found" error. This will make the handling of the error more complicated for our client.

Also, an even bigger problem: We don't want just any error to be returned to the client for security reasons (we may be exposing sensitive information).

How do we fix this?

The best way to handle this is to use different implementations of the Error class accordingly for each case. This can be achieved by building our own custom implementations or installing a library that already contains all the implementations of Error we need.

For REST APIs I like to use throw.js. It's a really simple module that contains Errors matching the most common HTTP status codes. Each error defined by this module also includes the status code as a property.

Let's see how the previous example will look like using throw.js:

/* --- product.service.js --- */const error = require('throw.js');const getById = async (id) => {    const product = await productModel.findById(id);    if (!product)        throw new error.NotFound('Product not found');    return product;}/* --- product.controller.js --- */const error = require('throw.js');const getById = async (req, res) => {    try {        const product = await productService.getById(req.params.id);        return product;    }    catch (err) {        if (err instanceof error.NotFound)            res.status(err.statusCode).json({ error: err.message });        else            res.status(500).json({ error: 'Unexpected error' });    }}

In this second approach we've achieved two things:

  • Our controller now has enough information to understand the error and act accordingly.
  • The REST API client will now also receive a status code that will also help them handle the error.

And we can even take this further by building a global error handler or middleware that handles all errors, so that we can clear that code from the controller. But that's a thing for another article.

Here's another module that implements the most common error types: node-common-errors.

Thoughts?

Were this 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!


Original Link: https://dev.to/paulasantamaria/refactoring-node-js-part-1-42fe

Share this article:    Share on Facebook
View Full Article

Dev To

An online community for sharing and discovering great ideas, having debates, and making friends

More About this Source Visit Dev To