Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
82 changes: 82 additions & 0 deletions docs/reference/promise/promisify.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
# promisify

Converts a callback-based function to a Promise-based function.

Takes a function that accepts a Node.js-style callback `(error, result) => void` as its last argument and returns a new function that returns a Promise. This is useful for modernizing legacy codebases that use callback patterns.

## Signature

```typescript
function promisify<Args extends unknown[], Result>(
fn: (...args: [...Args, Callback<Result>]) => void,
options?: PromisifyOptions
): (...args: Args) => Promise<Result>;
```

### Parameters

- `fn` (`(...args: [...Args, Callback<Result>]) => void`): A function that accepts a callback as its last argument. The callback should follow the Node.js convention: `(error, result) => void`.
- `options` (`PromisifyOptions`, optional): Configuration options.
- `context` (`object`, optional): The `this` context to bind when calling the function. Useful for object methods.

### Returns

(`(...args: Args) => Promise<Result>`): A new function that returns a Promise. The Promise resolves with the callback's result value, or rejects with the callback's error.

## Examples

### Basic usage

```typescript
import { promisify } from 'es-toolkit/promise';

function readFile(path: string, callback: (err: Error | null, data: string) => void) {
// simulate async file reading
setTimeout(() => callback(null, 'file content'), 100);
}

const readFileAsync = promisify(readFile);
const data = await readFileAsync('example.txt');
console.log(data); // 'file content'
```

### With context binding

When working with object methods that depend on `this`, use the `context` option:

```typescript
import { promisify } from 'es-toolkit/promise';

const redis = {
host: 'localhost',
get(key: string, callback: (err: Error | null, value: string) => void) {
// uses this.host
callback(null, `value from ${this.host}`);
},
};

// Without context, 'this' would be undefined
const redisGet = promisify(redis.get, { context: redis });
const value = await redisGet('myKey');
console.log(value); // 'value from localhost'
```

### Error handling

Errors passed to the callback are converted to Promise rejections:

```typescript
import { promisify } from 'es-toolkit/promise';

function failingOperation(callback: (err: Error | null, result: string) => void) {
callback(new Error('Operation failed'), '');
}

const failingOperationAsync = promisify(failingOperation);

try {
await failingOperationAsync();
} catch (error) {
console.error(error.message); // 'Operation failed'
}
```
50 changes: 50 additions & 0 deletions src/promise/asCallback.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import { describe, it, expect, vi } from 'vitest';
import { asCallback, nodeify } from './asCallback';

describe('asCallback', () => {
it('invokes callback with result on success', async () => {
const cb = vi.fn();
await asCallback(Promise.resolve(42), cb);
await Promise.resolve();
expect(cb).toHaveBeenCalledWith(null, 42);
});

it('invokes callback with error on rejection', async () => {
const cb = vi.fn();
const error = new Error('test error');
const promise = Promise.reject(error);

asCallback(promise, cb).catch(() => {
// Suppress unhandled rejection
});
await Promise.resolve();

expect(cb).toHaveBeenCalledWith(error, undefined);
});

it('converts non-Error rejections to Error instances', async () => {
const cb = vi.fn();
const promise = Promise.reject('string error');

asCallback(promise, cb).catch(() => {
// Suppress unhandled rejection
});
await Promise.resolve();

expect(cb).toHaveBeenCalled();
const [err] = cb.mock.calls[0];
expect(err).toBeInstanceOf(Error);
expect(err.message).toBe('string error');
});

it('returns the original promise for chaining', async () => {
const cb = vi.fn();
const promise = Promise.resolve('value');
const result = asCallback(promise, cb);
expect(result).toBe(promise);
});

it('nodeify is an alias for asCallback', () => {
expect(nodeify).toBe(asCallback);
});
});
74 changes: 74 additions & 0 deletions src/promise/asCallback.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
/**
* A Node.js-style callback function type.
*
* @template Result - The type of the result value on success.
*/
export type NodeStyleCallback<Result> = (err: Error | null, result: Result) => void;

/**
* Options for the asCallback function.
*/
export interface AsCallbackOptions {
/**
* If true, errors thrown in the callback won't be re-thrown.
* @default false
*/
suppressErrors?: boolean;
}
Comment on lines +8 to +17
Copy link

Copilot AI Dec 15, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The AsCallbackOptions interface defines a suppressErrors option, but this option is never used in the asCallback function implementation. The option should either be implemented or removed from the interface to avoid confusing users who might try to use this non-functional feature.

Suggested change
/**
* Options for the asCallback function.
*/
export interface AsCallbackOptions {
/**
* If true, errors thrown in the callback won't be re-thrown.
* @default false
*/
suppressErrors?: boolean;
}

Copilot uses AI. Check for mistakes.

/**
* Registers a Node.js-style callback on a promise.
*
* This function attaches a callback to a promise, invoking it when the promise
* settles. On success, the callback receives `(null, result)`. On failure, it
* receives `(error, undefined)`.
*
* @template Result - The type of the resolved value.
* @param {Promise<Result>} promise - The promise to attach the callback to.
* @param {NodeStyleCallback<Result>} callback - The Node.js-style callback function.
* @returns {Promise<Result>} The original promise (for chaining).
*
* @example
* // Basic usage
* import { asCallback } from 'es-toolkit/promise';
*
* const promise = Promise.resolve(42);
* asCallback(promise, (err, result) => {
* if (err) {
* console.error('Error:', err);
* } else {
* console.log('Result:', result); // Result: 42
* }
* });
*
* @example
* // Error handling
* const failingPromise = Promise.reject(new Error('Something went wrong'));
* asCallback(failingPromise, (err, result) => {
* if (err) {
* console.error('Error:', err.message); // Error: Something went wrong
* }
* });
*/
export function asCallback<Result>(
promise: Promise<Result>,
callback: NodeStyleCallback<Result>
): Promise<Result> {
Comment on lines +53 to +56
Copy link

Copilot AI Dec 15, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The asCallback function signature declares a parameter callback but the JSDoc references an options parameter that doesn't exist. The function should either accept an AsCallbackOptions parameter or the AsCallbackOptions interface should be removed since it's not used.

Copilot uses AI. Check for mistakes.
promise.then(
result => {
queueMicrotask(() => callback(null, result));
},
err => {
const error = err instanceof Error ? err : new Error(String(err));
queueMicrotask(() => callback(error, undefined as Result));
Copy link

Copilot AI Dec 15, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Similar issue: passing undefined as Result to the callback on error may cause type safety issues. The callback signature expects a Result type as the second parameter, but undefined is being forced through a type assertion. This could lead to runtime issues if callbacks don't properly handle undefined values.

Copilot uses AI. Check for mistakes.
}
);
return promise;
}

/**
* Alias for asCallback.
*
* @see {@link asCallback}
*/
export const nodeify = asCallback;
116 changes: 116 additions & 0 deletions src/promise/asCallbackAll.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
import { describe, it, expect } from 'vitest';
import { asCallbackAll } from './asCallbackAll';

describe('asCallbackAll', () => {
it('creates Callback methods from async methods', () =>
new Promise<void>(done => {
const api = {
async echo(msg: string): Promise<string> {
return msg;
},
};
const callbackApi = asCallbackAll(api);
callbackApi.echoCallback('hello', (err, result) => {
expect(err).toBeNull();
expect(result).toBe('hello');
done();
});
}));

it('preserves original async methods', async () => {
const api = {
async echo(msg: string): Promise<string> {
return msg;
},
};
const callbackApi = asCallbackAll(api);

// Original async method still works
await expect(callbackApi.echo('test')).resolves.toBe('test');
});

it('handles errors correctly', () =>
new Promise<void>(done => {
const api = {
async fail(): Promise<string> {
throw new Error('test error');
},
};
const callbackApi = asCallbackAll(api);
callbackApi.failCallback((err, _result) => {
expect(err).toBeInstanceOf(Error);
expect(err?.message).toBe('test error');
done();
});
}));

it('respects exclude option', () => {
const api = {
async included(): Promise<string> {
return 'included';
},
async excluded(): Promise<string> {
return 'excluded';
},
};
const callbackApi = asCallbackAll(api, { exclude: ['excluded'] });
expect('includedCallback' in callbackApi).toBe(true);
expect('excludedCallback' in callbackApi).toBe(false);
});

it('respects include option', () => {
const api = {
async included(): Promise<string> {
return 'included';
},
async notIncluded(): Promise<string> {
return 'not included';
},
};
const callbackApi = asCallbackAll(api, { include: ['included'] });
expect('includedCallback' in callbackApi).toBe(true);
expect('notIncludedCallback' in callbackApi).toBe(false);
});

it('uses custom suffix', () => {
const api = {
async echo(msg: string): Promise<string> {
return msg;
},
};
const callbackApi = asCallbackAll(api, { suffix: 'Cb' });
expect('echoCb' in callbackApi).toBe(true);
expect('echoCallback' in callbackApi).toBe(false);
});

it('preserves context', () =>
new Promise<void>(done => {
const api = {
value: 42,
async getValue(): Promise<number> {
return this.value;
},
};
const callbackApi = asCallbackAll(api);
callbackApi.getValueCallback((err, result) => {
expect(err).toBeNull();
expect(result).toBe(42);
done();
});
}));

it('works with multiple arguments', () =>
new Promise<void>(done => {
const api = {
async add(a: number, b: number): Promise<number> {
return a + b;
},
};
const callbackApi = asCallbackAll(api);
callbackApi.addCallback(5, 3, (err, result) => {
expect(err).toBeNull();
expect(result).toBe(8);
done();
});
}));
});
Loading