asynchronous programming
node.js
custom functions
javascript
async functions

How do you create custom asynchronous functions in node.js?

Interview Questions practice on Codemia

Over 8,000 real interview questions from top companies, searchable by company and role.

Browse interview questions

Introduction

In modern Node.js, the normal way to create a custom asynchronous function is to return a promise, usually by writing an async function. The key is not just making the function asynchronous, but giving it a clear contract for success, failure, and cancellation.

Start with async and await

An async function automatically returns a promise. If it returns a value, the promise resolves. If it throws, the promise rejects.

Example:

javascript
1const fs = require("node:fs/promises");
2
3async function readConfig(path) {
4  const text = await fs.readFile(path, "utf8");
5  return JSON.parse(text);
6}
7
8readConfig("./config.json")
9  .then((config) => console.log(config))
10  .catch((err) => console.error(err.message));

This is the simplest pattern for custom async functions that wrap I/O.

Wrapping Callback APIs

Older Node APIs still use callbacks. You can expose a cleaner modern interface by wrapping them in a promise:

javascript
1const fsCallback = require("node:fs");
2
3function readTextFile(path, encoding = "utf8") {
4  return new Promise((resolve, reject) => {
5    fsCallback.readFile(path, encoding, (err, data) => {
6      if (err) {
7        reject(err);
8        return;
9      }
10      resolve(data);
11    });
12  });
13}

Now callers can use await readTextFile("notes.txt") instead of nested callbacks.

Add Timeout or Cancellation

Real asynchronous code needs failure boundaries. If an operation can hang, add timeout or abort support instead of assuming it will always complete.

Example timeout wrapper:

javascript
1function withTimeout(promise, ms, label = "operation") {
2  let timer;
3
4  const timeout = new Promise((_, reject) => {
5    timer = setTimeout(() => {
6      reject(new Error(`${label} timed out after ${ms}ms`));
7    }, ms);
8  });
9
10  return Promise.race([promise, timeout]).finally(() => clearTimeout(timer));
11}

Using it:

javascript
1async function loadUser(id) {
2  const response = await withTimeout(
3    fetch(`https://jsonplaceholder.typicode.com/users/${id}`),
4    3000,
5    "loadUser"
6  );
7
8  if (!response.ok) {
9    throw new Error(`HTTP ${response.status}`);
10  }
11
12  return response.json();
13}

This gives the function a predictable upper bound instead of letting it wait indefinitely.

Designing Good Async Helpers

A custom async function should answer three questions clearly:

  • what value does it resolve with
  • what errors can it reject with
  • can callers cancel or time-limit it

That is more important than whether the implementation uses async, Promise, or a wrapper around a callback.

For repeated tasks, compose helpers instead of building monolithic functions. For example, separate "fetch JSON," "validate payload," and "transform result" into smaller async pieces that can be tested individually.

Error Handling Pattern

Inside async functions, normal try and catch works well:

javascript
1async function loadSettings(path) {
2  try {
3    const text = await fs.readFile(path, "utf8");
4    return JSON.parse(text);
5  } catch (err) {
6    throw new Error(`failed to load settings: ${err.message}`);
7  }
8}

This keeps callers on a consistent promise-based interface while still letting you add domain-specific error context.

Avoid Unbounded Concurrency

Creating custom async functions is easy. Creating safe high-throughput async workflows is harder. A common mistake is calling Promise.all on a huge list and overwhelming the filesystem, database, or remote API.

If the function will be used repeatedly, think about concurrency limits early. Good async APIs do not just "work"; they behave predictably under load.

Common Pitfalls

The most common mistake is forgetting to return or await a promise. The function looks asynchronous, but the caller receives unexpected timing or unhandled rejections.

Another issue is mixing callbacks and promises in the same public API. Pick one style for the function contract and stick to it.

Developers also often swallow errors inside catch blocks and return fallback values silently. That makes debugging much harder than an explicit rejection.

Finally, do not assume asynchronous means parallel. Async functions can improve structure and responsiveness without automatically creating safe concurrency.

Summary

  • In Node.js, custom asynchronous functions should usually return promises.
  • 'async and await are the cleanest way to write them.'
  • Wrap legacy callback APIs once and expose modern promise-based helpers.
  • Add timeout or cancellation behavior for operations that may hang.
  • Define success values and failure semantics clearly so callers can use the function safely.

Related reading
Free course
Beginner
7 lessons
2 hours
Tackling System Design Interview Problems

A short course that equips you with the skills to approach system design interviews methodically.

Start the free course
Track what you have practised

A free account saves your progress, solutions and study plan across every problem on Codemia.

Interview Questions practice on Codemia

Over 8,000 real interview questions from top companies, searchable by company and role.

Browse interview questions

All Rights Reserved.