asynchronous programming
callback to async conversion
JavaScript
async/await
programming tutorials

Wrapping a callback-based class to an async one

Interview Questions practice on Codemia

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

Browse interview questions

Introduction

When a class exposes callback-based methods, the cleanest way to make it usable with async and await is to wrap each callback method in a Promise. The important details are preserving this, translating callback errors into rejected promises, and deciding how to expose multiple callback results.

The Basic Conversion Pattern

Suppose you start with an older API like this:

javascript
1class LegacyStore {
2  getUser(id, callback) {
3    setTimeout(() => {
4      if (id <= 0) {
5        callback(new Error("invalid id"));
6        return;
7      }
8      callback(null, { id, name: "Ava" });
9    }, 50);
10  }
11}

That API is not wrong, but it is awkward to compose because every call needs a nested callback.

The direct wrapper is a promise:

javascript
1class AsyncStore {
2  constructor(store) {
3    this.store = store;
4  }
5
6  getUser(id) {
7    return new Promise((resolve, reject) => {
8      this.store.getUser(id, (error, user) => {
9        if (error) {
10          reject(error);
11          return;
12        }
13        resolve(user);
14      });
15    });
16  }
17}
18
19(async () => {
20  const store = new AsyncStore(new LegacyStore());
21  console.log(await store.getUser(1));
22})();

Once the method returns a promise, consumers can use await naturally.

Preserve the Original Instance Correctly

The most common wrapping bug is losing this. If the original method depends on instance state, extracting it without binding can break it.

This is unsafe:

javascript
const method = this.store.getUser;
method(1, callback);

This is safe because the method is invoked on the original instance:

javascript
this.store.getUser(1, callback);

If you need to store the function first, bind it.

Mapping Callback Shapes

Not every callback uses the Node-style (error, result) convention. Some APIs call back with only a result, and others provide multiple success values.

For multiple values, resolve an object or array:

javascript
1class LegacyMath {
2  divide(a, b, callback) {
3    callback(null, Math.floor(a / b), a % b);
4  }
5}
6
7function divideAsync(service, a, b) {
8  return new Promise((resolve, reject) => {
9    service.divide(a, b, (error, quotient, remainder) => {
10      if (error) return reject(error);
11      resolve({ quotient, remainder });
12    });
13  });
14}

That makes the async API predictable instead of pretending the callback had only one result.

Wrapper Class or Utility Function

If you are modernizing a whole class, a wrapper class keeps the old and new interfaces separate. If you only need one or two methods, standalone utility functions may be enough.

Use a wrapper class when:

  • many methods need conversion
  • you want to hide the callback API entirely
  • the async version should become the main public interface

Use a helper function when the migration is small. A small internal helper such as toPromise(fn) can also reduce repetition, but only when the callback signature is consistent enough to wrap safely.

Common Pitfalls

The biggest mistake is forgetting to reject the promise on callback errors. That creates async methods that hang forever instead of failing.

Another mistake is losing the original this binding when calling the legacy method.

A third mistake is wrapping a callback API that can fire multiple times as if it were a one-shot promise. Promises represent one final result, so event-style callbacks need a different abstraction such as an async iterator or event emitter wrapper.

Summary

  • Wrap callback-based methods in Promise to make them usable with async and await.
  • Preserve the original instance context when calling legacy methods.
  • Convert callback errors into rejected promises.
  • For multiple success values, resolve an object or array instead of dropping data.
  • Use wrapper classes for broader migrations and utility functions for isolated conversions.

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.