📚 5 min read
Custom Promise Implementation ​
Overview ​
Learn how to build your own Promise implementation from scratch. This guide covers the core functionality of Promises, including state management, chaining, and error handling.
Implementation ​
typescript
class CustomPromise<T> {
private state: 'pending' | 'fulfilled' | 'rejected' = 'pending';
private value: T | null = null;
private error: any = null;
private thenCallbacks: Array<(value: T) => any> = [];
private catchCallbacks: Array<(error: any) => any> = [];
private finallyCallbacks: Array<() => any> = [];
constructor(
executor: (
resolve: (value: T) => void,
reject: (reason?: any) => void
) => void
) {
try {
executor(
(value) => this.resolve(value),
(reason) => this.reject(reason)
);
} catch (error) {
this.reject(error);
}
}
private resolve(value: T): void {
if (this.state !== 'pending') return;
this.state = 'fulfilled';
this.value = value;
this.executeThenCallbacks();
this.executeFinallyCallbacks();
}
private reject(reason: any): void {
if (this.state !== 'pending') return;
this.state = 'rejected';
this.error = reason;
this.executeCatchCallbacks();
this.executeFinallyCallbacks();
}
private executeThenCallbacks(): void {
if (this.state !== 'fulfilled') return;
this.thenCallbacks.forEach((callback) => {
try {
callback(this.value!);
} catch (error) {
// Handle errors in callbacks
console.error('Error in then callback:', error);
}
});
this.thenCallbacks = [];
}
private executeCatchCallbacks(): void {
if (this.state !== 'rejected') return;
this.catchCallbacks.forEach((callback) => {
try {
callback(this.error);
} catch (error) {
console.error('Error in catch callback:', error);
}
});
this.catchCallbacks = [];
}
private executeFinallyCallbacks(): void {
this.finallyCallbacks.forEach((callback) => {
try {
callback();
} catch (error) {
console.error('Error in finally callback:', error);
}
});
this.finallyCallbacks = [];
}
then<U>(onFulfilled: (value: T) => U | PromiseLike<U>): CustomPromise<U> {
return new CustomPromise<U>((resolve, reject) => {
const callback = (value: T) => {
try {
const result = onFulfilled(value);
if (result instanceof CustomPromise) {
result
.then((value) => resolve(value))
.catch((error) => reject(error));
} else {
resolve(result);
}
} catch (error) {
reject(error);
}
};
if (this.state === 'fulfilled') {
callback(this.value!);
} else {
this.thenCallbacks.push(callback as any);
}
});
}
catch<U>(onRejected: (error: any) => U | PromiseLike<U>): CustomPromise<U> {
return new CustomPromise<U>((resolve, reject) => {
const callback = (error: any) => {
try {
const result = onRejected(error);
if (result instanceof CustomPromise) {
result
.then((value) => resolve(value))
.catch((error) => reject(error));
} else {
resolve(result);
}
} catch (error) {
reject(error);
}
};
if (this.state === 'rejected') {
callback(this.error);
} else {
this.catchCallbacks.push(callback as any);
}
});
}
finally(onFinally: () => void): CustomPromise<T> {
if (this.state !== 'pending') {
onFinally();
return this;
}
this.finallyCallbacks.push(onFinally);
return this;
}
// Static methods
static resolve<U>(value: U): CustomPromise<U> {
return new CustomPromise<U>((resolve) => resolve(value));
}
static reject<U>(reason: any): CustomPromise<U> {
return new CustomPromise<U>((_, reject) => reject(reason));
}
}
Usage Examples ​
Basic Usage ​
typescript
const promise = new CustomPromise<string>((resolve, reject) => {
setTimeout(() => {
if (Math.random() > 0.5) {
resolve('Success!');
} else {
reject(new Error('Failed!'));
}
}, 1000);
});
promise
.then((result) => {
console.log('Success:', result);
return result.toUpperCase();
})
.catch((error) => {
console.error('Error:', error);
return 'Recovered';
})
.finally(() => {
console.log('Cleanup operations');
});
Chaining Promises ​
typescript
const fetchUser = (id: string) =>
new CustomPromise<{ id: string; name: string }>((resolve) => {
setTimeout(() => {
resolve({ id, name: 'John Doe' });
}, 1000);
});
const fetchUserPosts = (userId: string) =>
new CustomPromise<string[]>((resolve) => {
setTimeout(() => {
resolve(['Post 1', 'Post 2', 'Post 3']);
}, 1000);
});
fetchUser('123')
.then((user) => {
console.log('User:', user);
return fetchUserPosts(user.id);
})
.then((posts) => {
console.log('Posts:', posts);
})
.catch((error) => {
console.error('Error:', error);
});
Error Handling ​
typescript
const validateUser = (user: { age: number }) =>
new CustomPromise<string>((resolve, reject) => {
if (user.age < 18) {
reject(new Error('User must be 18 or older'));
} else {
resolve('User is valid');
}
});
validateUser({ age: 16 })
.then((result) => {
console.log(result);
})
.catch((error) => {
console.error('Validation failed:', error.message);
// Handle the error gracefully
return 'Invalid user';
});
Key Features ​
State Management
- Maintains promise state (pending, fulfilled, rejected)
- Ensures state transitions are one-way only
- Stores resolved value or rejection reason
Callback Handling
- Supports multiple then/catch/finally callbacks
- Executes callbacks in order of registration
- Handles errors in callbacks gracefully
Promise Chaining
- Supports return values from then/catch callbacks
- Handles nested promises automatically
- Maintains proper error propagation
Error Handling
- Catches synchronous errors in executor function
- Supports error recovery in catch callbacks
- Provides stack trace preservation
Best Practices ​
Error Handling
typescript// Always catch potential errors promise.then(handleSuccess).catch(handleError).finally(cleanup);
Type Safety
typescript// Use TypeScript generics for type safety const promise = new CustomPromise<number>((resolve) => { resolve(42); });
Resource Cleanup
typescript// Use finally for cleanup operations const connection = await connect(); processData(connection).finally(() => { connection.close(); });
Common Pitfalls ​
Forgetting Error Handling
typescript// Bad: No error handling promise.then(handleSuccess); // Good: With error handling promise.then(handleSuccess).catch(handleError);
Nested Promise Chains
typescript// Bad: Promise nesting promise.then((result) => { return anotherPromise().then((newResult) => { // More nesting }); }); // Good: Flat promise chain promise .then((result) => anotherPromise()) .then((newResult) => { // Handle result });
Losing Error Context
typescript// Bad: Error context lost promise.catch(() => 'Error occurred'); // Good: Preserve error context promise.catch((error) => { console.error('Original error:', error); return 'Error occurred'; });
Performance Considerations ​
Memory Management
- Clear callback references after execution
- Avoid storing unnecessary state
- Use WeakMap for storing metadata if needed
Execution Order
- Promises are always asynchronous
- Use microtasks for better performance
- Consider batching multiple promise resolutions
Error Handling Overhead
- Balance between error handling and performance
- Use error boundaries for groups of operations
- Consider error sampling in production