📚 6 min read
Promisifying Examples ​
This page demonstrates practical examples of converting callback-based APIs to Promise-based APIs.
Basic Promisification ​
typescript
// Basic promisify implementation
function promisify<T>(
fn: (...args: any[]) => void
): (...args: any[]) => Promise<T> {
return function (...args: any[]): Promise<T> {
return new Promise((resolve, reject) => {
fn(...args, (error: Error | null, result: T) => {
if (error) {
reject(error);
} else {
resolve(result);
}
});
});
};
}
// Usage with Node.js fs module
import { readFile } from 'fs';
const readFileAsync = promisify<string>(readFile);
try {
const content = await readFileAsync('file.txt', 'utf8');
console.log('File content:', content);
} catch (error) {
console.error('Error reading file:', error);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
Advanced Promisification ​
typescript
class Promisifier {
private static defaultOptions: PromisifyOptions = {
multiArgs: false,
thisArg: undefined,
preserveName: true,
};
static promisify<T>(
fn: Function,
options: Partial<PromisifyOptions> = {}
): (...args: any[]) => Promise<T> {
const opts = { ...this.defaultOptions, ...options };
const promisified = function (this: any, ...args: any[]): Promise<T> {
const ctx = opts.thisArg ?? this;
return new Promise((resolve, reject) => {
const callback = (error: Error | null, ...results: any[]) => {
if (error) {
reject(error);
return;
}
if (opts.multiArgs) {
resolve(results as any);
} else {
resolve(results[0]);
}
};
try {
fn.apply(ctx, [...args, callback]);
} catch (error) {
reject(error);
}
});
};
if (opts.preserveName) {
Object.defineProperty(promisified, 'name', {
value: `${fn.name}Async`,
configurable: true,
});
}
return promisified;
}
static promisifyAll(obj: any, options: Partial<PromisifyOptions> = {}): void {
const props = Object.getOwnPropertyNames(obj);
for (const prop of props) {
const descriptor = Object.getOwnPropertyDescriptor(obj, prop);
if (descriptor && typeof obj[prop] === 'function') {
obj[`${prop}Async`] = this.promisify(obj[prop], options);
}
}
}
}
// Usage with event emitter
import { EventEmitter } from 'events';
class DatabaseClient extends EventEmitter {
query(
sql: string,
callback: (error: Error | null, result: any) => void
): void {
// Simulate database query
setTimeout(() => {
if (sql.toLowerCase().includes('error')) {
callback(new Error('Query failed'), null);
} else {
callback(null, { rows: [{ id: 1, name: 'Test' }] });
}
}, 100);
}
transaction(
operations: string[],
callback: (error: Error | null, results: any[]) => void
): void {
// Simulate transaction
setTimeout(() => {
const results = operations.map((op) => ({
operation: op,
status: 'success',
}));
callback(null, results);
}, 200);
}
}
// Promisify individual method
const db = new DatabaseClient();
const queryAsync = Promisifier.promisify<any>(db.query.bind(db));
// Promisify all methods
Promisifier.promisifyAll(db);
// Usage
try {
const result = await queryAsync('SELECT * FROM users');
console.log('Query result:', result);
const transactionResults = await db.transactionAsync([
'INSERT INTO users',
'UPDATE users',
]);
console.log('Transaction results:', transactionResults);
} catch (error) {
console.error('Database error:', error);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
Real-World Example: Legacy API Wrapper ​
typescript
class LegacyApiWrapper {
private api: LegacyApi;
private promisified: Map<string, Function> = new Map();
constructor(api: LegacyApi) {
this.api = api;
this.promisifyMethods();
}
private promisifyMethods(): void {
const methodNames = Object.getOwnPropertyNames(
Object.getPrototypeOf(this.api)
);
for (const name of methodNames) {
if (name === 'constructor') continue;
const method = this.api[name];
if (typeof method === 'function') {
this.promisified.set(name, this.promisifyMethod(name, method));
}
}
}
private promisifyMethod(name: string, method: Function): Function {
return async (...args: any[]): Promise<any> => {
return new Promise((resolve, reject) => {
const callback = (error: Error | null, ...results: any[]) => {
if (error) {
// Enhance error with context
const enhancedError = new ApiError(error.message, {
method: name,
arguments: args,
originalError: error,
});
reject(enhancedError);
return;
}
// Transform results if needed
const transformed = this.transformResults(name, results);
resolve(transformed);
};
try {
method.apply(this.api, [...args, callback]);
} catch (error) {
reject(
new ApiError('Method invocation failed', {
method: name,
arguments: args,
originalError: error,
})
);
}
});
};
}
private transformResults(method: string, results: any[]): any {
// Apply method-specific transformations
switch (method) {
case 'getUsers':
return this.transformUsers(results[0]);
case 'getOrders':
return this.transformOrders(results[0]);
default:
return results[0];
}
}
private transformUsers(users: any[]): User[] {
return users.map((user) => ({
id: user.user_id,
name: user.user_name,
email: user.user_email,
createdAt: new Date(user.created_timestamp),
}));
}
private transformOrders(orders: any[]): Order[] {
return orders.map((order) => ({
id: order.order_id,
userId: order.user_id,
amount: parseFloat(order.amount),
status: order.status.toLowerCase(),
createdAt: new Date(order.created_timestamp),
}));
}
async call<T>(method: string, ...args: any[]): Promise<T> {
const promisified = this.promisified.get(method);
if (!promisified) {
throw new Error(`Method ${method} not found`);
}
return promisified(...args);
}
}
// Usage
class LegacyApi {
getUsers(callback: (error: Error | null, users: any[]) => void): void {
// Simulate legacy API call
setTimeout(() => {
callback(null, [
{
user_id: 1,
user_name: 'John Doe',
user_email: 'john@example.com',
created_timestamp: '2023-01-01T00:00:00Z',
},
]);
}, 100);
}
getOrders(
userId: number,
callback: (error: Error | null, orders: any[]) => void
): void {
// Simulate legacy API call
setTimeout(() => {
callback(null, [
{
order_id: 1,
user_id: userId,
amount: '99.99',
status: 'COMPLETED',
created_timestamp: '2023-01-02T00:00:00Z',
},
]);
}, 100);
}
}
const legacyApi = new LegacyApi();
const wrapper = new LegacyApiWrapper(legacyApi);
// Modern async/await usage
async function getUserData(userId: number) {
try {
const users = await wrapper.call<User[]>('getUsers');
const orders = await wrapper.call<Order[]>('getOrders', userId);
return {
user: users.find((u) => u.id === userId),
orders,
};
} catch (error) {
console.error('Failed to fetch user data:', error);
throw error;
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
Best Practices ​
Error handling:
typescriptclass PromiseError extends Error { constructor( message: string, public readonly context: { method: string; args: any[]; originalError?: Error; } ) { super(message); this.name = 'PromiseError'; } toJSON() { return { name: this.name, message: this.message, context: this.context, }; } } function promisifyWithErrors<T>( fn: Function, errorMap: Map<string, typeof Error> ): (...args: any[]) => Promise<T> { return function (...args: any[]): Promise<T> { return new Promise((resolve, reject) => { fn(...args, (error: Error | null, result: T) => { if (error) { const ErrorClass = errorMap.get(error.name) ?? PromiseError; reject( new ErrorClass(error.message, { method: fn.name, args, originalError: error, }) ); } else { resolve(result); } }); }); }; }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45Timeout handling:
typescriptfunction promisifyWithTimeout<T>( fn: Function, timeout: number ): (...args: any[]) => Promise<T> { return function (...args: any[]): Promise<T> { return new Promise((resolve, reject) => { const timeoutId = setTimeout(() => { reject(new Error('Operation timed out')); }, timeout); fn(...args, (error: Error | null, result: T) => { clearTimeout(timeoutId); if (error) { reject(error); } else { resolve(result); } }); }); }; }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21Resource cleanup:
typescriptfunction promisifyWithCleanup<T>( fn: Function, cleanup: () => void ): (...args: any[]) => Promise<T> { return function (...args: any[]): Promise<T> { return new Promise((resolve, reject) => { fn(...args, (error: Error | null, result: T) => { try { cleanup(); } catch (cleanupError) { console.error('Cleanup failed:', cleanupError); } if (error) { reject(error); } else { resolve(result); } }); }); }; }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22Event handling:
typescriptfunction promisifyEvent<T>( emitter: EventEmitter, eventName: string ): Promise<T> { return new Promise((resolve, reject) => { function handler(data: T) { cleanup(); resolve(data); } function errorHandler(error: Error) { cleanup(); reject(error); } function cleanup() { emitter.removeListener(eventName, handler); emitter.removeListener('error', errorHandler); } emitter.once(eventName, handler); emitter.once('error', errorHandler); }); }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24