Guidelines
Concurrency

Concurrency

Avoid long-running synchronous tasks

Synchronous tasks that take a long time to execute can block the main thread and prevent the user interface from updating or responding to user input. To ensure a smooth and responsive user experience, it is important to minimize the time spent on long-running synchronous tasks and break them up into smaller chunks if possible.

Avoid callbacks

Callbacks are a common mechanism for handling asynchronous tasks in JavaScript, but they can lead to "callback hell" - a situation where the code becomes difficult to read and maintain due to the nested structure of the callbacks. To avoid this, it is generally recommended to use async-await or Promise instead of callbacks for handling asynchronous tasks. These mechanisms provide a more readable and structured syntax for writing asynchronous code.

Prefer async-await to work with Promises

async-await and Promise are both mechanisms for handling asynchronous code in JavaScript. They are related but distinct concepts, and can be used together or separately depending on the needs of the task at hand.

Here is a brief overview of the differences between async-await and Promise:

  • async-await: async-await is a syntax for working with asynchronous code that makes it easier to read and write. The async keyword is used to declare an asynchronous function, and the await keyword is used to pause the execution of the function until a promise is resolved.
  • Promise: a Promise is an object that represents the result of an asynchronous operation. A Promise can be in one of three states: pending, fulfilled, or rejected. If the asynchronous operation is successful, the Promise is fulfilled with a value. If the operation fails, the Promise is rejected with an error.

async-await and Promise can be used together to handle asynchronous code in a more structured and readable way. async-await makes it easier to work with Promise objects, allowing you to use a synchronous-style syntax for asynchronous operations.

const getData = () =>
  new Promise((resolve, reject) => {
    setTimeout(() => resolve("data"), 1000);
  });
 
const main = async () => {
  try {
    const data = await getData();
    console.log(data);
  } catch (error) {
    console.error(error);
  }
};
 
main();

Consider using streams for handling large amounts of data

Streams are a mechanism for handling large amounts of data efficiently in JavaScript. Instead of loading the entire dataset into memory at once, streams allow you to process the data in small chunks, reducing the memory overhead and improving the performance of your application. There are several types of streams available in JavaScript, including readable, writable, and transform streams, and they can be used with the stream (opens in a new tab) module or with libraries such as RxJS (opens in a new tab).

const fs = require("fs");
const stream = require("stream");
 
// Create a readable stream from a file
const fileStream = fs.createReadStream("./large-file.txt");
 
// Create a writable stream to a file
const outputStream = fs.createWriteStream("./output.txt");
 
// Create a transform stream that modifies the data as it flows through
const transformStream = new stream.Transform({
  transform(chunk, encoding, callback) {
    // Modify the chunk of data and pass it to the callback
    const modifiedChunk = chunk.toString().toUpperCase();
    callback(null, modifiedChunk);
  },
});
 
// Pipe the readable stream through the transform stream and into the writable stream
fileStream.pipe(transformStream).pipe(outputStream);
 
// Listen for the 'finish' event to know when the output stream has finished writing
outputStream.on("finish", () => {
  console.log("Finished writing to output file");
});

Avoid using shared mutable state

Shared mutable state can lead to race conditions and other concurrency issues. To avoid these issues, try to avoid using shared mutable state whenever possible and instead use immutable data structures or other techniques to manage shared state.

Avoid locks and mutexes

Locks and mutexes are both synchronization mechanisms that can be used to control access to shared resources, but they work in slightly different ways. Locks allow multiple threads to acquire the lock simultaneously, while mutexes only allow one thread to acquire the lock at a time.

Locks and mutexes can be useful for synchronizing access to shared resources, but they can also lead to deadlocks and other concurrency issues if used improperly. Avoid using these synchronization mechanisms unless absolutely necessary.

That being said, there may be some cases where it is necessary to use locks or mutexes in a Node.js program, such as when interacting with external resources that are not thread-safe or when integrating with existing code that uses locks or mutexes. In these cases, it is important to use caution, to carefully consider the potential performance impacts of using these synchronization mechanisms and to make sure to release them as soon as possible to avoid blocking other tasks.

Last updated on