Prerequisites
- Basic TypeScript syntax and types 📝
- Understanding of Promises and async/await ⚡
- Callback patterns and function types 🔄
What you'll learn
- Master promisification techniques for legacy callback APIs 🔄
- Build type-safe promise wrappers with proper error handling 🛡️
- Create reusable promisification utilities and patterns 🛠️
- Modernize codebases from callback hell to promise heaven ✨
🎯 Introduction
Welcome to the bridge between the old and new worlds of asynchronous JavaScript! 🎉 In this guide, we’ll master the art of converting callback-based functions into modern, promise-based APIs that work seamlessly with async/await.
You’ll discover how to transform legacy callback patterns into clean, type-safe promises that eliminate callback hell and enable modern async patterns. Whether you’re working with Node.js APIs 🌐, legacy libraries 📚, or third-party code 🔧, mastering promisification is essential for building modern TypeScript applications.
By the end of this tutorial, you’ll be converting callback chaos into promise paradise! 🔄 Let’s dive in! 🏊♂️
📚 Understanding Callbacks vs Promises
🤔 What is Promisification?
Promisification is like giving your old callback-based functions a modern makeover 💅. Think of it as translating from an old language to a new one - the same functionality, but with better syntax and error handling!
In TypeScript terms, promisification provides:
- ✨ Modern syntax - use async/await instead of nested callbacks
- 🚀 Better composition - chain operations cleanly
- 🛡️ Improved error handling - unified try/catch patterns
- 📦 Type safety - maintain strong typing throughout the conversion
💡 Why Promisify Callbacks?
Here’s why promisification is a game-changer:
- Eliminate Callback Hell 🌋: Flatten deeply nested callback pyramids
- Modern Patterns ✨: Use async/await for cleaner, more readable code
- Better Error Handling 🛡️: Unified error propagation with try/catch
- Composability 🔗: Easily chain and combine async operations
- Future-Proof 🚀: Align with modern JavaScript best practices
Real-world example: Converting Node.js fs.readFile
callback to a promise enables clean async/await usage instead of nested callback pyramids! 📁
🔧 Basic Promisification Patterns
📝 Simple Manual Promisification
Let’s start with fundamental promisification techniques:
// 🎯 Basic promisification pattern
// Converting Node.js style callbacks to promises
import { readFile, writeFile } from 'fs';
import { promisify } from 'util';
// 🔄 Manual promisification approach
const readFilePromise = (filename: string, encoding: BufferEncoding = 'utf8'): Promise<string> => {
return new Promise((resolve, reject) => {
console.log(`📖 Reading file: ${filename}`);
// 🔄 Call the original callback-based function
readFile(filename, encoding, (error, data) => {
if (error) {
console.error(`💥 File read error: ${error.message}`);
reject(error); // 🚫 Reject promise on error
} else {
console.log(`✅ File read successfully: ${data.length} characters`);
resolve(data); // ✅ Resolve promise with data
}
});
});
};
// 🔧 Type-safe promisification with better error handling
const writeFilePromise = (
filename: string,
data: string,
encoding: BufferEncoding = 'utf8'
): Promise<void> => {
return new Promise<void>((resolve, reject) => {
console.log(`✍️ Writing file: ${filename}`);
writeFile(filename, data, encoding, (error) => {
if (error) {
console.error(`💥 File write error: ${error.message}`);
reject(new FileWriteError(`Failed to write ${filename}`, error));
} else {
console.log(`✅ File written successfully: ${filename}`);
resolve(); // ✅ Resolve with no value for void operations
}
});
});
};
// 🏗️ Custom error types for better error handling
class FileWriteError extends Error {
constructor(message: string, public originalError: Error) {
super(message);
this.name = 'FileWriteError';
}
}
// ✨ Usage with async/await
const processFile = async (inputFile: string, outputFile: string): Promise<void> => {
try {
// 🔄 Read input file
const content = await readFilePromise(inputFile);
// 🔄 Process content
const processedContent = content.toUpperCase();
console.log(`🔄 Processing complete: ${processedContent.length} characters`);
// 🔄 Write output file
await writeFilePromise(outputFile, processedContent);
console.log('🎉 File processing complete!');
} catch (error) {
console.error('💥 File processing failed:', error.message);
throw error;
}
};
// 🎮 Example usage
// processFile('input.txt', 'output.txt');
🛠️ Generic Promisification Utility
Let’s create a powerful, reusable promisification utility:
// 🚀 Advanced generic promisification utility
// Handles various callback patterns with type safety
// 🏗️ Callback type definitions
type NodeCallback<T> = (error: Error | null, result: T) => void;
type ErrorFirstCallback<T> = (error: Error | null, ...results: T[]) => void;
type ResultFirstCallback<T> = (result: T, error?: Error) => void;
// 🔧 Generic promisify function for Node.js style callbacks
const promisifyNodeStyle = <TArgs extends any[], TResult>(
fn: (...args: [...TArgs, NodeCallback<TResult>]) => void
) => {
return (...args: TArgs): Promise<TResult> => {
return new Promise<TResult>((resolve, reject) => {
console.log(`🔄 Promisifying function call with ${args.length} arguments`);
// 📞 Call original function with promise-based callback
fn(...args, (error: Error | null, result: TResult) => {
if (error) {
console.error(`💥 Callback error: ${error.message}`);
reject(error);
} else {
console.log(`✅ Callback success`);
resolve(result);
}
});
});
};
};
// 🔧 Generic promisify for multiple result callbacks
const promisifyMultiResult = <TArgs extends any[], TResults extends any[]>(
fn: (...args: [...TArgs, ErrorFirstCallback<TResults>]) => void
) => {
return (...args: TArgs): Promise<TResults> => {
return new Promise<TResults>((resolve, reject) => {
console.log(`🔄 Promisifying multi-result function`);
fn(...args, (error: Error | null, ...results: TResults) => {
if (error) {
console.error(`💥 Multi-result callback error: ${error.message}`);
reject(error);
} else {
console.log(`✅ Multi-result callback success: ${results.length} results`);
resolve(results);
}
});
});
};
};
// 🔧 Promisify for result-first callbacks (like some legacy APIs)
const promisifyResultFirst = <TArgs extends any[], TResult>(
fn: (...args: [...TArgs, ResultFirstCallback<TResult>]) => void
) => {
return (...args: TArgs): Promise<TResult> => {
return new Promise<TResult>((resolve, reject) => {
console.log(`🔄 Promisifying result-first function`);
fn(...args, (result: TResult, error?: Error) => {
if (error) {
console.error(`💥 Result-first callback error: ${error.message}`);
reject(error);
} else {
console.log(`✅ Result-first callback success`);
resolve(result);
}
});
});
};
};
// 🏗️ Example usage with type inference
import { exec } from 'child_process';
import { readdir, stat } from 'fs';
// 📂 Promisified directory operations
const readdirPromise = promisifyNodeStyle(readdir);
const statPromise = promisifyNodeStyle(stat);
const execPromise = promisifyNodeStyle(exec);
// 📊 Advanced file system operations
const getDirectoryInfo = async (dirPath: string): Promise<DirectoryInfo> => {
try {
console.log(`📂 Analyzing directory: ${dirPath}`);
// 📄 Read directory contents
const files = await readdirPromise(dirPath);
console.log(`📄 Found ${files.length} items`);
// 📊 Get detailed stats for each item
const fileStats = await Promise.all(
files.map(async (file) => {
const fullPath = `${dirPath}/${file}`;
const stats = await statPromise(fullPath);
return {
name: file,
path: fullPath,
size: stats.size,
isDirectory: stats.isDirectory(),
isFile: stats.isFile(),
lastModified: stats.mtime
};
})
);
// 📈 Calculate summary statistics
const totalSize = fileStats.reduce((sum, file) => sum + file.size, 0);
const fileCount = fileStats.filter(f => f.isFile).length;
const dirCount = fileStats.filter(f => f.isDirectory).length;
console.log(`✅ Directory analysis complete: ${fileCount} files, ${dirCount} dirs, ${totalSize} bytes`);
return {
path: dirPath,
totalSize,
fileCount,
directoryCount: dirCount,
items: fileStats
};
} catch (error) {
console.error(`💥 Directory analysis failed: ${error.message}`);
throw new DirectoryAnalysisError(`Failed to analyze directory ${dirPath}`, error);
}
};
// 🏗️ Type definitions
interface FileInfo {
name: string;
path: string;
size: number;
isDirectory: boolean;
isFile: boolean;
lastModified: Date;
}
interface DirectoryInfo {
path: string;
totalSize: number;
fileCount: number;
directoryCount: number;
items: FileInfo[];
}
class DirectoryAnalysisError extends Error {
constructor(message: string, public originalError: Error) {
super(message);
this.name = 'DirectoryAnalysisError';
}
}
🌐 Real-World Promisification Examples
📡 Network Operations
Let’s promisify common network and I/O operations:
// 🌐 Comprehensive network and database promisification
// Real-world examples with robust error handling
import { request, RequestOptions } from 'http';
import { connect, Socket } from 'net';
// 🌐 HTTP request promisification with type safety
interface HttpResponse {
statusCode: number;
headers: Record<string, string | string[]>;
body: string;
}
const httpRequest = (url: string, options: RequestOptions = {}): Promise<HttpResponse> => {
return new Promise<HttpResponse>((resolve, reject) => {
console.log(`🌐 Making HTTP request to: ${url}`);
const req = request(url, options, (res) => {
let body = '';
// 📦 Collect response data
res.on('data', (chunk) => {
body += chunk;
});
// ✅ Request complete
res.on('end', () => {
console.log(`✅ HTTP request complete: ${res.statusCode}`);
resolve({
statusCode: res.statusCode || 0,
headers: res.headers,
body
});
});
// 💥 Response error
res.on('error', (error) => {
console.error(`💥 Response error: ${error.message}`);
reject(new HttpResponseError('Response error', error));
});
});
// 💥 Request error
req.on('error', (error) => {
console.error(`💥 Request error: ${error.message}`);
reject(new HttpRequestError('Request failed', error));
});
// ⏰ Request timeout
req.setTimeout(10000, () => {
console.error('⏰ Request timeout');
req.destroy();
reject(new HttpTimeoutError('Request timeout after 10 seconds'));
});
req.end();
});
};
// 🔌 TCP connection promisification
interface ConnectionInfo {
socket: Socket;
localAddress: string;
localPort: number;
remoteAddress: string;
remotePort: number;
}
const connectTcp = (port: number, host: string = 'localhost'): Promise<ConnectionInfo> => {
return new Promise<ConnectionInfo>((resolve, reject) => {
console.log(`🔌 Connecting to ${host}:${port}`);
const socket = new Socket();
// ✅ Connection successful
socket.connect(port, host, () => {
console.log(`✅ Connected to ${host}:${port}`);
resolve({
socket,
localAddress: socket.localAddress || '',
localPort: socket.localPort || 0,
remoteAddress: socket.remoteAddress || '',
remotePort: socket.remotePort || 0
});
});
// 💥 Connection error
socket.on('error', (error) => {
console.error(`💥 Connection error: ${error.message}`);
reject(new ConnectionError(`Failed to connect to ${host}:${port}`, error));
});
// ⏰ Connection timeout
socket.setTimeout(5000, () => {
console.error(`⏰ Connection timeout to ${host}:${port}`);
socket.destroy();
reject(new ConnectionTimeoutError(`Connection timeout to ${host}:${port}`));
});
});
};
// 📨 Email sending promisification (simplified example)
interface EmailOptions {
to: string;
subject: string;
body: string;
from?: string;
}
interface EmailResult {
messageId: string;
accepted: string[];
rejected: string[];
response: string;
}
// 🎭 Mock email service for demonstration
const sendEmail = (options: EmailOptions): Promise<EmailResult> => {
return new Promise<EmailResult>((resolve, reject) => {
console.log(`📨 Sending email to: ${options.to}`);
console.log(`📧 Subject: ${options.subject}`);
// 🎭 Simulate async email sending
setTimeout(() => {
const success = Math.random() > 0.1; // 90% success rate
if (success) {
const result: EmailResult = {
messageId: `msg-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`,
accepted: [options.to],
rejected: [],
response: '250 Message accepted'
};
console.log(`✅ Email sent successfully: ${result.messageId}`);
resolve(result);
} else {
console.error('💥 Email sending failed');
reject(new EmailError('SMTP server unavailable', options.to));
}
}, 1000 + Math.random() * 2000); // 1-3 second delay
});
};
// 🏗️ Custom error classes
class HttpRequestError extends Error {
constructor(message: string, public originalError: Error) {
super(message);
this.name = 'HttpRequestError';
}
}
class HttpResponseError extends Error {
constructor(message: string, public originalError: Error) {
super(message);
this.name = 'HttpResponseError';
}
}
class HttpTimeoutError extends Error {
constructor(message: string) {
super(message);
this.name = 'HttpTimeoutError';
}
}
class ConnectionError extends Error {
constructor(message: string, public originalError: Error) {
super(message);
this.name = 'ConnectionError';
}
}
class ConnectionTimeoutError extends Error {
constructor(message: string) {
super(message);
this.name = 'ConnectionTimeoutError';
}
}
class EmailError extends Error {
constructor(message: string, public recipient: string) {
super(message);
this.name = 'EmailError';
}
}
// 🚀 Advanced usage patterns
const performNetworkOperations = async (): Promise<void> => {
try {
console.log('🚀 Starting network operations...');
// 🌐 Parallel HTTP requests
const [response1, response2] = await Promise.all([
httpRequest('http://httpbin.org/json'),
httpRequest('http://httpbin.org/uuid')
]);
console.log(`📊 Received ${response1.body.length} and ${response2.body.length} bytes`);
// 📨 Send notification email
const emailResult = await sendEmail({
to: '[email protected]',
subject: 'Network Operations Complete',
body: `Successfully fetched data: ${response1.body.length + response2.body.length} total bytes`
});
console.log(`📧 Notification sent: ${emailResult.messageId}`);
console.log('✅ All network operations completed successfully');
} catch (error) {
console.error('💥 Network operations failed:', error.message);
// 🔄 Retry logic could be implemented here
throw error;
}
};
🗄️ Database Operations
Let’s promisify database operations with proper connection management:
// 🗄️ Database operations promisification
// Comprehensive example with connection pooling and transactions
// 🎭 Mock database interface for demonstration
interface DatabaseConnection {
id: string;
isConnected: boolean;
lastUsed: Date;
}
interface QueryResult<T = any> {
rows: T[];
rowCount: number;
executionTime: number;
}
interface TransactionContext {
connection: DatabaseConnection;
queries: string[];
rollback: () => Promise<void>;
commit: () => Promise<void>;
}
// 🔄 Database connection promisification
const connectDatabase = (connectionString: string): Promise<DatabaseConnection> => {
return new Promise<DatabaseConnection>((resolve, reject) => {
console.log(`🗄️ Connecting to database...`);
// 🎭 Simulate connection delay
setTimeout(() => {
const success = Math.random() > 0.05; // 95% success rate
if (success) {
const connection: DatabaseConnection = {
id: `conn-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`,
isConnected: true,
lastUsed: new Date()
};
console.log(`✅ Database connected: ${connection.id}`);
resolve(connection);
} else {
console.error('💥 Database connection failed');
reject(new DatabaseConnectionError('Unable to connect to database'));
}
}, 500 + Math.random() * 1000); // 0.5-1.5 second delay
});
};
// 📊 Query execution promisification
const executeQuery = <T = any>(
connection: DatabaseConnection,
query: string,
params: any[] = []
): Promise<QueryResult<T>> => {
return new Promise<QueryResult<T>>((resolve, reject) => {
if (!connection.isConnected) {
reject(new DatabaseError('Connection is not active'));
return;
}
console.log(`📊 Executing query: ${query.substring(0, 50)}...`);
console.log(`🔧 Parameters: ${JSON.stringify(params)}`);
const startTime = Date.now();
// 🎭 Simulate query execution
setTimeout(() => {
const success = Math.random() > 0.02; // 98% success rate
if (success) {
// 🎭 Generate mock results
const mockRows = Array.from({ length: Math.floor(Math.random() * 10) + 1 }, (_, i) => ({
id: i + 1,
name: `Record ${i + 1}`,
value: Math.random() * 100,
created: new Date()
}));
const result: QueryResult<T> = {
rows: mockRows as T[],
rowCount: mockRows.length,
executionTime: Date.now() - startTime
};
// 📊 Update connection usage
connection.lastUsed = new Date();
console.log(`✅ Query executed: ${result.rowCount} rows in ${result.executionTime}ms`);
resolve(result);
} else {
console.error('💥 Query execution failed');
reject(new QueryExecutionError('Query failed to execute', query));
}
}, 100 + Math.random() * 500); // 0.1-0.6 second delay
});
};
// 🔄 Transaction management promisification
const beginTransaction = (connection: DatabaseConnection): Promise<TransactionContext> => {
return new Promise<TransactionContext>((resolve, reject) => {
if (!connection.isConnected) {
reject(new DatabaseError('Connection is not active'));
return;
}
console.log('🔄 Beginning transaction...');
// 🎭 Simulate transaction start
setTimeout(() => {
const success = Math.random() > 0.01; // 99% success rate
if (success) {
const context: TransactionContext = {
connection,
queries: [],
rollback: () => rollbackTransaction(context),
commit: () => commitTransaction(context)
};
console.log('✅ Transaction started');
resolve(context);
} else {
console.error('💥 Transaction start failed');
reject(new TransactionError('Failed to start transaction'));
}
}, 50 + Math.random() * 100);
});
};
// 🔙 Transaction rollback
const rollbackTransaction = (context: TransactionContext): Promise<void> => {
return new Promise<void>((resolve, reject) => {
console.log(`🔙 Rolling back transaction (${context.queries.length} queries)`);
setTimeout(() => {
console.log('✅ Transaction rolled back');
resolve();
}, 100 + Math.random() * 200);
});
};
// ✅ Transaction commit
const commitTransaction = (context: TransactionContext): Promise<void> => {
return new Promise<void>((resolve, reject) => {
console.log(`✅ Committing transaction (${context.queries.length} queries)`);
setTimeout(() => {
const success = Math.random() > 0.02; // 98% success rate
if (success) {
console.log('✅ Transaction committed successfully');
resolve();
} else {
console.error('💥 Transaction commit failed');
reject(new TransactionError('Failed to commit transaction'));
}
}, 100 + Math.random() * 300);
});
};
// 🏗️ Database error classes
class DatabaseConnectionError extends Error {
constructor(message: string) {
super(message);
this.name = 'DatabaseConnectionError';
}
}
class DatabaseError extends Error {
constructor(message: string) {
super(message);
this.name = 'DatabaseError';
}
}
class QueryExecutionError extends Error {
constructor(message: string, public query: string) {
super(message);
this.name = 'QueryExecutionError';
}
}
class TransactionError extends Error {
constructor(message: string) {
super(message);
this.name = 'TransactionError';
}
}
// 🚀 Complete database operation example
const performDatabaseOperations = async (): Promise<void> => {
let connection: DatabaseConnection | null = null;
try {
console.log('🚀 Starting database operations...');
// 🗄️ Connect to database
connection = await connectDatabase('postgresql://localhost:5432/myapp');
// 📊 Execute simple queries
const usersResult = await executeQuery(connection, 'SELECT * FROM users WHERE active = $1', [true]);
console.log(`👤 Found ${usersResult.rowCount} active users`);
// 🔄 Transaction example
const transaction = await beginTransaction(connection);
try {
// 📝 Execute queries within transaction
await executeQuery(connection, 'INSERT INTO users (name, email) VALUES ($1, $2)', ['John Doe', '[email protected]']);
transaction.queries.push('INSERT INTO users');
await executeQuery(connection, 'UPDATE user_stats SET total_users = total_users + 1', []);
transaction.queries.push('UPDATE user_stats');
// ✅ Commit transaction
await transaction.commit();
console.log('✅ User creation transaction completed');
} catch (transactionError) {
console.error('💥 Transaction error:', transactionError.message);
// 🔙 Rollback on error
await transaction.rollback();
throw transactionError;
}
console.log('✅ All database operations completed successfully');
} catch (error) {
console.error('💥 Database operations failed:', error.message);
throw error;
} finally {
// 🧹 Cleanup connection
if (connection) {
console.log(`🧹 Closing database connection: ${connection.id}`);
connection.isConnected = false;
}
}
};
🛠️ Advanced Promisification Utilities
🏭 Complete Promisification Toolkit
Let’s build a comprehensive toolkit for all promisification needs:
// 🏭 Complete promisification toolkit
// Production-ready utilities with extensive type safety
// 🔧 Configuration options for promisification
interface PromisifyOptions {
timeout?: number;
retries?: number;
retryDelay?: number;
abortSignal?: AbortSignal;
}
// 🎯 Advanced promisify with timeout and retry logic
const promisifyAdvanced = <TArgs extends any[], TResult>(
fn: (...args: [...TArgs, NodeCallback<TResult>]) => void,
options: PromisifyOptions = {}
) => {
return async (...args: TArgs): Promise<TResult> => {
const { timeout = 30000, retries = 0, retryDelay = 1000, abortSignal } = options;
let lastError: Error;
// 🔄 Retry loop
for (let attempt = 0; attempt <= retries; attempt++) {
try {
if (abortSignal?.aborted) {
throw new Error('Operation was aborted');
}
console.log(`🔄 Attempt ${attempt + 1}/${retries + 1}`);
// 🎯 Create promise with timeout
const result = await Promise.race([
// 📞 Original function call
new Promise<TResult>((resolve, reject) => {
fn(...args, (error: Error | null, result: TResult) => {
if (error) {
reject(error);
} else {
resolve(result);
}
});
}),
// ⏰ Timeout promise
new Promise<never>((_, reject) => {
setTimeout(() => {
reject(new PromiseTimeoutError(`Operation timeout after ${timeout}ms`));
}, timeout);
})
]);
console.log(`✅ Operation succeeded on attempt ${attempt + 1}`);
return result;
} catch (error) {
lastError = error as Error;
console.error(`💥 Attempt ${attempt + 1} failed: ${lastError.message}`);
// 🔄 Wait before retry (except on last attempt)
if (attempt < retries) {
console.log(`⏳ Waiting ${retryDelay}ms before retry...`);
await new Promise(resolve => setTimeout(resolve, retryDelay));
}
}
}
throw new PromiseRetryError(`Operation failed after ${retries + 1} attempts`, lastError);
};
};
// 📦 Batch promisification utility
const promisifyBatch = <T extends Record<string, Function>>(
obj: T,
options: PromisifyOptions = {}
): PromisifiedObject<T> => {
console.log(`📦 Batch promisifying ${Object.keys(obj).length} functions`);
const promisified = {} as PromisifiedObject<T>;
for (const [key, fn] of Object.entries(obj)) {
if (typeof fn === 'function') {
console.log(`🔄 Promisifying: ${key}`);
promisified[key as keyof T] = promisifyAdvanced(fn, options) as any;
}
}
return promisified;
};
// 🔄 Conditional promisification (only if not already a promise)
const promisifyConditional = <TArgs extends any[], TResult>(
fn: (...args: TArgs) => TResult | Promise<TResult>
) => {
return async (...args: TArgs): Promise<TResult> => {
console.log('🔄 Conditional promisification...');
const result = fn(...args);
// 🔍 Check if result is already a promise
if (result instanceof Promise) {
console.log('✅ Function already returns a promise');
return result;
} else {
console.log('🔄 Converting result to promise');
return Promise.resolve(result);
}
};
};
// 🎭 Mock function creator for testing promisification
const createMockCallbackFunction = <T>(
result: T,
delay: number = 100,
errorRate: number = 0
) => {
return (callback: NodeCallback<T>) => {
console.log(`🎭 Mock function called (${delay}ms delay, ${errorRate * 100}% error rate)`);
setTimeout(() => {
if (Math.random() < errorRate) {
console.error('🎭 Mock function simulating error');
callback(new Error('Simulated error'), null as any);
} else {
console.log('🎭 Mock function simulating success');
callback(null, result);
}
}, delay);
};
};
// 🏗️ Type utilities for promisification
type NodeCallback<T> = (error: Error | null, result: T) => void;
type PromisifiedFunction<T extends Function> = T extends (
...args: [...infer Args, NodeCallback<infer Result>]
) => void
? (...args: Args) => Promise<Result>
: never;
type PromisifiedObject<T> = {
[K in keyof T]: T[K] extends Function ? PromisifiedFunction<T[K]> : T[K];
};
// 🏗️ Error classes for advanced promisification
class PromiseTimeoutError extends Error {
constructor(message: string) {
super(message);
this.name = 'PromiseTimeoutError';
}
}
class PromiseRetryError extends Error {
constructor(message: string, public lastError: Error) {
super(message);
this.name = 'PromiseRetryError';
}
}
// 🚀 Comprehensive example usage
const demonstrateAdvancedPromisification = async (): Promise<void> => {
try {
console.log('🚀 Demonstrating advanced promisification...');
// 🎭 Create mock functions
const slowFunction = createMockCallbackFunction('Slow result', 2000, 0.1);
const fastFunction = createMockCallbackFunction('Fast result', 500, 0.05);
const unreliableFunction = createMockCallbackFunction('Unreliable result', 1000, 0.3);
// 🔄 Promisify with different options
const slowPromise = promisifyAdvanced(slowFunction, {
timeout: 5000,
retries: 2,
retryDelay: 1000
});
const fastPromise = promisifyAdvanced(fastFunction, {
timeout: 2000,
retries: 1,
retryDelay: 500
});
const unreliablePromise = promisifyAdvanced(unreliableFunction, {
timeout: 3000,
retries: 3,
retryDelay: 500
});
// 📊 Execute operations in parallel
const results = await Promise.allSettled([
slowPromise(),
fastPromise(),
unreliablePromise()
]);
// 📈 Analyze results
results.forEach((result, index) => {
const functionName = ['slow', 'fast', 'unreliable'][index];
if (result.status === 'fulfilled') {
console.log(`✅ ${functionName} function succeeded: ${result.value}`);
} else {
console.error(`💥 ${functionName} function failed: ${result.reason.message}`);
}
});
// 📦 Batch promisification example
const legacyApi = {
getUserData: createMockCallbackFunction({ id: 1, name: 'John' }, 800, 0.1),
updateUser: createMockCallbackFunction({ success: true }, 1200, 0.15),
deleteUser: createMockCallbackFunction({ deleted: true }, 600, 0.05)
};
const promisifiedApi = promisifyBatch(legacyApi, {
timeout: 5000,
retries: 2,
retryDelay: 1000
});
// 🔄 Use promisified API
const userData = await promisifiedApi.getUserData();
console.log('👤 User data:', userData);
console.log('✅ Advanced promisification demonstration complete');
} catch (error) {
console.error('💥 Demonstration failed:', error.message);
throw error;
}
};
💡 Best Practices & Patterns
🎯 Promisification Best Practices
Here are essential patterns for effective promisification:
// ✅ Best practices for promisification
// Production-ready patterns and common pitfalls to avoid
// ✅ DO: Preserve original error information
const promisifyWithErrorPreservation = <TArgs extends any[], TResult>(
fn: (...args: [...TArgs, NodeCallback<TResult>]) => void
) => {
return (...args: TArgs): Promise<TResult> => {
return new Promise<TResult>((resolve, reject) => {
fn(...args, (error: Error | null, result: TResult) => {
if (error) {
// ✅ Preserve original error with stack trace
const enhancedError = new Error(`Promisified function failed: ${error.message}`);
enhancedError.stack = error.stack;
enhancedError.cause = error;
reject(enhancedError);
} else {
resolve(result);
}
});
});
};
};
// ✅ DO: Handle undefined/null results properly
const promisifyWithNullHandling = <TArgs extends any[], TResult>(
fn: (...args: [...TArgs, NodeCallback<TResult | null>]) => void
) => {
return (...args: TArgs): Promise<TResult> => {
return new Promise<TResult>((resolve, reject) => {
fn(...args, (error: Error | null, result: TResult | null) => {
if (error) {
reject(error);
} else if (result === null || result === undefined) {
reject(new Error('Function returned null or undefined result'));
} else {
resolve(result);
}
});
});
};
};
// ✅ DO: Add proper typing for optional parameters
const promisifyWithOptionals = <
TRequired extends any[],
TOptional extends any[],
TResult
>(
fn: (...args: [...TRequired, ...Partial<TOptional>, NodeCallback<TResult>]) => void
) => {
return (...args: [...TRequired, ...Partial<TOptional>]): Promise<TResult> => {
return new Promise<TResult>((resolve, reject) => {
fn(...args, (error: Error | null, result: TResult) => {
if (error) {
reject(error);
} else {
resolve(result);
}
});
});
};
};
// ❌ DON'T: Swallow errors silently
const promisifyBadExample = <TArgs extends any[], TResult>(
fn: (...args: [...TArgs, NodeCallback<TResult>]) => void
) => {
return (...args: TArgs): Promise<TResult> => {
return new Promise<TResult>((resolve) => {
fn(...args, (error: Error | null, result: TResult) => {
if (error) {
console.error('Error occurred:', error); // ❌ Don't just log
resolve(null as any); // ❌ Don't resolve with null
} else {
resolve(result);
}
});
});
};
};
// ✅ DO: Handle multiple callback patterns
interface MultiCallbackOptions {
callbackPosition?: 'last' | 'first' | number;
errorPosition?: 'first' | 'last' | number;
}
const promisifyFlexible = <TArgs extends any[], TResult>(
fn: Function,
options: MultiCallbackOptions = {}
) => {
const { callbackPosition = 'last', errorPosition = 'first' } = options;
return (...args: TArgs): Promise<TResult> => {
return new Promise<TResult>((resolve, reject) => {
const callback = (...callbackArgs: any[]) => {
const errorIndex = errorPosition === 'first' ? 0 : callbackArgs.length - 1;
const error = callbackArgs[errorIndex];
if (error) {
reject(error);
} else {
// 📦 Return all non-error arguments as result
const results = callbackArgs.filter((_, index) => index !== errorIndex);
resolve(results.length === 1 ? results[0] : results);
}
};
if (callbackPosition === 'first') {
fn(callback, ...args);
} else if (callbackPosition === 'last') {
fn(...args, callback);
} else if (typeof callbackPosition === 'number') {
const newArgs = [...args];
newArgs.splice(callbackPosition, 0, callback);
fn(...newArgs);
}
});
};
};
// 🔄 Resource management with automatic cleanup
const promisifyWithCleanup = <TArgs extends any[], TResult, TResource>(
createResource: (...args: TArgs) => TResource,
operation: (resource: TResource, callback: NodeCallback<TResult>) => void,
cleanup: (resource: TResource) => void
) => {
return (...args: TArgs): Promise<TResult> => {
return new Promise<TResult>((resolve, reject) => {
const resource = createResource(...args);
console.log('🔄 Resource created, starting operation...');
operation(resource, (error: Error | null, result: TResult) => {
// 🧹 Always cleanup, regardless of success or failure
try {
cleanup(resource);
console.log('🧹 Resource cleaned up');
} catch (cleanupError) {
console.error('💥 Cleanup failed:', cleanupError.message);
}
// 📤 Handle operation result
if (error) {
reject(error);
} else {
resolve(result);
}
});
});
};
};
// 📊 Performance monitoring wrapper
const promisifyWithMetrics = <TArgs extends any[], TResult>(
fn: (...args: [...TArgs, NodeCallback<TResult>]) => void,
metricName: string
) => {
return (...args: TArgs): Promise<TResult> => {
return new Promise<TResult>((resolve, reject) => {
const startTime = process.hrtime.bigint();
console.log(`📊 Starting ${metricName}...`);
fn(...args, (error: Error | null, result: TResult) => {
const endTime = process.hrtime.bigint();
const duration = Number(endTime - startTime) / 1000000; // Convert to ms
if (error) {
console.error(`💥 ${metricName} failed after ${duration.toFixed(2)}ms: ${error.message}`);
reject(error);
} else {
console.log(`✅ ${metricName} completed in ${duration.toFixed(2)}ms`);
resolve(result);
}
});
});
};
};
// 🔄 Comprehensive usage example
const demonstrateBestPractices = async (): Promise<void> => {
try {
console.log('🚀 Demonstrating promisification best practices...');
// ✅ Proper error preservation
const mockApiCall = (data: string, callback: NodeCallback<string>) => {
setTimeout(() => {
if (data === 'error') {
const error = new Error('API call failed');
error.stack = 'CustomStack: at mockApiCall...';
callback(error, null as any);
} else {
callback(null, `Processed: ${data}`);
}
}, 100);
};
const promisifiedApi = promisifyWithErrorPreservation(mockApiCall);
// 📊 Test successful call
const successResult = await promisifiedApi('test data');
console.log('✅ Success result:', successResult);
// 📊 Test error handling
try {
await promisifiedApi('error');
} catch (error) {
console.log('🔍 Error properly preserved:', error.message);
console.log('📚 Original cause available:', error.cause?.message);
}
// 📈 Performance monitoring example
const slowOperation = (delay: number, callback: NodeCallback<string>) => {
setTimeout(() => {
callback(null, `Operation completed after ${delay}ms`);
}, delay);
};
const monitoredOperation = promisifyWithMetrics(slowOperation, 'SlowOperation');
const performanceResult = await monitoredOperation(1500);
console.log('📊 Monitored result:', performanceResult);
console.log('✅ Best practices demonstration complete');
} catch (error) {
console.error('💥 Best practices demonstration failed:', error.message);
throw error;
}
};
🏆 Summary
Congratulations! 🎉 You’ve mastered the art of promisifying callback-based functions in TypeScript! Here’s what you’ve accomplished:
🎯 Key Takeaways
- Modern Async Patterns ✨: Transform legacy callbacks into modern promise-based APIs
- Type Safety 🛡️: Maintain strong typing throughout the promisification process
- Error Handling 🚨: Implement robust error propagation and recovery patterns
- Advanced Utilities 🛠️: Build reusable promisification tools with retry and timeout logic
- Best Practices 📋: Follow proven patterns for production-ready promisification
🛠️ What You Can Build
- Legacy API Modernization 🔄: Convert old callback-based libraries to modern async/await
- Database Abstraction Layers 🗄️: Create promise-based database interfaces
- File System Operations 📁: Modernize Node.js fs operations with proper error handling
- Network Communication 🌐: Transform callback-based HTTP and socket operations
- Third-Party Integrations 🔧: Wrap external services with consistent promise interfaces
🚀 Next Steps
Ready to take your async skills even further? Consider exploring:
- Observables with RxJS 📡: Reactive programming patterns for complex data flows
- Streams in Node.js 🌊: Handle large data processing with streaming APIs
- Worker Threads 👥: Parallel processing with modern async patterns
- GraphQL Integration 🔄: Type-safe API queries with modern async patterns
You’re now equipped to bridge the gap between legacy callback code and modern async patterns, creating maintainable and type-safe applications! 🎯 Keep promisifying! 🔄✨