+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Part 106 of 355

🔄 Callbacks to Promises: Promisifying Functions Mastery

Master the art of converting callback-based functions to promises in TypeScript for modern async patterns and cleaner code architecture 🚀

🚀Intermediate
18 min read

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:

  1. Eliminate Callback Hell 🌋: Flatten deeply nested callback pyramids
  2. Modern Patterns ✨: Use async/await for cleaner, more readable code
  3. Better Error Handling 🛡️: Unified error propagation with try/catch
  4. Composability 🔗: Easily chain and combine async operations
  5. 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

  1. Modern Async Patterns ✨: Transform legacy callbacks into modern promise-based APIs
  2. Type Safety 🛡️: Maintain strong typing throughout the promisification process
  3. Error Handling 🚨: Implement robust error propagation and recovery patterns
  4. Advanced Utilities 🛠️: Build reusable promisification tools with retry and timeout logic
  5. 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! 🔄✨