Sync, callback, and promise based APIs in Node.js

May 11, 2023#node#comparison

In Node.js, there are three common approaches to handle API response: synchronous, callback-based, and promise-based.

Node.js has gradually shifted towards Promise-based APIs and introduced the util.promisify utility to convert callback-based functions into Promise-based ones. Additionally, the introduction of async/await syntax in newer versions of Node.js simplifies working with Promises, making asynchronous code resemble synchronous code in terms of readability and flow control.

Synchronous APIs

Synchronous APIs, also known as blocking APIs, are programming interfaces that perform operations in a synchronous manner. In Node.js, synchronous APIs are typically suffixed with “Sync” to differentiate them from their asynchronous counterparts.

const fs = require('fs');

// Synchronous API
const data = fs.readFileSync('file.txt', 'utf8');
console.log(data);
console.log('Done reading file');

When using synchronous APIs, the execution of code is halted until the operation is completed. The program waits for the API call to finish and then continues with the next line of code. This blocking behavior means that other operations or tasks cannot proceed until the synchronous operation is finished. Here are a few characteristics of synchronous APIs:

  • Blocking: Synchronous APIs block the execution of code until the operation completes.
  • Immediate Result: Synchronous APIs provide the result immediately, allowing the program to proceed.
  • Simplicity: Synchronous APIs can simplify code structure as they do not require callback functions or handling promises.
  • Potential for Delay: If a synchronous API call takes a long time to complete, it can cause the entire program to freeze or become unresponsive until the operation finishes.

It’s worth noting that using synchronous APIs can impact the responsiveness and scalability of your application, particularly in scenarios with heavy I/O or concurrent operations. Thus, it is generally recommended to favor asynchronous or non-blocking APIs in Node.js for better performance and scalability. However, there are some cases where synchronous APIs can be useful, such as:

  • Performing one-time initialization tasks at the start of the program
  • Writing simple scripts or command-line tools that do not need concurrency
  • Testing or debugging purposes

In general, synchronous APIs should be avoided or minimized in Node.js, unless it is necessary for some reason.

Callback-based APIs

Callback-based APIs, also known as asynchronous APIs or Node.js-style callbacks, are a common pattern in Node.js for handling asynchronous operations. These APIs rely on passing callback functions as arguments to asynchronous functions, which are then invoked once the operation is completed or an error occurs.

const fs = require('fs');

// Callback-based API
fs.readFile('file.txt', 'utf8', (err, data) => {
  if (err) {
    console.error(err);
  } else {
    console.log(data);
  }
});
console.log('Done reading file');

Here are the key characteristics of callback-based APIs:

  • Asynchronous Execution: Callback-based APIs allow for non-blocking execution of operations, enabling other parts of the program to continue while waiting for the asynchronous operation to complete.
  • Error-First Callbacks: The callback function typically follows an “error-first” convention, where the first argument of the callback is reserved for an error object. If an error occurs during the operation, it is passed as the first argument to the callback. If there is no error, this argument is usually null or undefined.
  • Result Handling: If the operation is successful, the result or value of the operation is passed as subsequent arguments to the callback function.
  • Control Flow Management: Callback-based APIs require explicit management of control flow, often leading to nested or chained callbacks (known as “callback hell” or “pyramid of doom”). This can make the code harder to read and maintain.
  • Error Handling: Errors need to be handled within the callback function itself. Developers typically check the error argument and handle errors accordingly.
  • Parallelism and Sequencing: Callback-based APIs can be used to perform parallel or sequential asynchronous operations by chaining or nesting callbacks.

It’s important to note that callback-based APIs can lead to callback hell, making code harder to read and maintain. To mitigate this issue, other asynchronous patterns like Promises or async/await syntax are often preferred in modern Node.js applications.

Promise-based APIs

Promise-based APIs, introduced in ECMAScript 2015 (ES6), are an alternative approach to handling asynchronous operations in JavaScript and Node.js. Promises provide a more structured and readable way to work with asynchronous code compared to callback-based APIs.

const fs = require("fs");

(async () => {
  try {
    await fs.promises.access("foo.txt", fs.constants.F_OK);
    // Do something
  } catch (err) {
    // Handle error
  }
})();

Here are the key characteristics of Promise-based APIs:

  • Asynchronous Operations: Promise-based APIs allow for non-blocking execution of operations, similar to callback-based APIs.
  • Promise Object: Promises represent the eventual completion (or failure) of an asynchronous operation, providing a cleaner separation between the initiation of an operation and handling its result.
  • Chaining and Sequencing: Promises can be chained together using .then() to perform sequential operations or handle subsequent transformations on the results.
  • Error Handling: Promises have built-in error handling capabilities. Errors can be caught and handled using .catch(), allowing for a centralized error handling approach.
  • Simplified Control Flow: Promise-based APIs facilitate a more linear and readable control flow compared to nested or chained callbacks.
  • Parallelism and Concurrency: Promises can be combined using Promise.all() or Promise.race() to execute multiple asynchronous operations concurrently or sequentially.
  • Compatibility: Promises are widely supported in modern JavaScript environments and can be used with libraries and tools that adopt the Promise interface.

With Promise-based APIs, you can handle asynchronous operations in a more readable and structured manner, making the code easier to understand and maintain. Promise-based APIs are widely adopted in modern JavaScript and Node.js development, and they serve as the foundation for other async patterns like async/await.