promise-breaker
promise-breaker is an ESM utility for library authors who must support both Node-style callbacks and Promises through one function. It can wrap callback code so callers may await it, wrap async code so legacy callers may pass a callback, or invoke a user-supplied function without knowing which completion style it uses. Version 7 is a TypeScript rewrite with declarations and no runtime dependencies, aimed mostly at compatibility layers and gradual legacy migrations.
Useful when dual callback and Promise support is an actual compatibility requirement, especially for a published library. For application code you control, standardize on async functions and leave this adapter out.
Use it if
- You publish a library whose established API must accept both callbacks and Promises during a migration
- You are converting a callback-heavy codebase gradually and cannot update every caller in one release
- Your extension API accepts user functions that may either return a Promise or call a Node-style callback
- You want generic TypeScript overloads without hand-writing the same adapter logic for many functions
- You control all callers and can standardize on async functions: native Promise code avoids a compatibility layer and ambiguous completion rules
- Your project uses CommonJS: version 7 declares type module and publishes an ESM entry point without a require export
- The callback function is variadic or has a function as its final real argument: automatic callback placement or detection can misclassify the call unless you use the lower-level helpers
- You need cancellation, timeouts, progress, multiple callback results, or non-Node callback conventions: the adapters model one error-first callback and one result
- You expect synchronous exceptions to become rejections in every path: the README states that promisify lets a synchronous throw escape synchronously
Setup reality
Install with `npm install promise-breaker` and import its named exports. Version 7 has no runtime dependencies and includes TypeScript declarations, but it is ESM-only because the package declares `type: module` and points its main entry at an ES module. There is no documented Node engine floor, so compatibility must be established in your own test matrix. The harder part is choosing the correct adapter. promisify always appends an internal callback; when a caller already supplied one, a normal fixed-arity implementation uses the caller's callback and ignores the extra one. The JavaScript wrapper still creates and returns a Promise in that branch, but the type overload says void and the unused Promise never settles, so callback callers must ignore the return value. callbackify assumes the last function argument is the callback unless `{ args: number }` tells it how many real arguments to expect. call and apply can accept either callback or Promise implementations, but an implementation that both invokes its callback and returns a Promise can create double-completion surprises in callback mode. None of the adapters add a timeout, cancellation, or protection against a callback that fires twice. Version 7 is also a complete rewrite: make and break remain aliases, while old option shapes and generated function-length behavior were removed. Test both calling styles, error paths, method `this` binding, and any variadic signatures before treating the wrapper as transparent.
Patterns
Offer Promise and callback calls from callback codepromisify-callback-function
import { promisify } from 'promise-breaker';
const readUser = promisify((id, done) => {
database.get(id, done);
});
const user = await readUser('42');The wrapped implementation must use a Node-style callback whose first argument is the error and second argument is the result.
Keep a legacy callback caller workingcall-promisified-with-callback
readUser('42', (error, user) => {
if (error) {
console.error(error);
return;
}
console.log(user);
});Ignore the return value in callback form. The implementation creates a Promise that does not settle when the caller's callback occupies the expected callback position.
Add callback support to an async functioncallbackify-async-function
import { callbackify } from 'promise-breaker';
const loadConfig = callbackify(async (file) => {
const text = await fs.promises.readFile(file, 'utf8');
return JSON.parse(text);
});
const config = await loadConfig('config.json');Without a callback, callbackify returns the original Promise from the async implementation.
Disambiguate a real function argumentprotect-function-argument
const mapAsync = callbackify({ args: 2 }, async (items, mapper) => {
return Promise.all(items.map(mapper));
});
const doubled = await mapAsync([1, 2], (n) => n * 2);Without `{ args: 2 }`, the wrapper would mistake mapper for the optional completion callback and remove it before calling the implementation.
Await either a callback or Promise implementationcall-unknown-async-style
import { call } from 'promise-breaker';
async function runPlugin(plugin, input) {
return call(plugin.transform, plugin, input);
}
const output = await runPlugin(plugin, source);Pass the owning object as thisArg for methods. The function must settle by callback or Promise; no timeout is added if it does neither.
Invoke an unknown-style function with an argument arrayapply-argument-array
import { apply } from 'promise-breaker';
const args = ['report.csv', { encoding: 'utf8' }];
const result = await apply(loader, loaderContext, args);apply appends a callback to the array. If loader returns a thenable, that returned thenable wins over the internally created callback Promise.
Adapt an unknown-style function to a callback callercall-unknown-with-callback
import { callWithCb } from 'promise-breaker';
callWithCb(plugin.transform, plugin, source, (error, output) => {
if (error) return done(error);
save(output, done);
});Do not pass implementations that both call their callback and return a Promise; callback mode may invoke the completion callback from both paths.
Control callback placement with addPromiseadd-promise-manually
import { addPromise } from 'promise-breaker';
function lookup(id, options, done) {
return addPromise(done, (finish) => {
legacyLookup(id, options, finish);
});
}
const record = await lookup('42', { fresh: true });Use this lower-level helper for variadic or unusual signatures where promisify's extra trailing callback is unsafe.
Control an async implementation with addCallbackadd-callback-manually
import { addCallback } from 'promise-breaker';
function calculate(input, done) {
return addCallback(done, async () => {
const value = await expensiveStep(input);
return value * 2;
});
}When done is supplied, addCallback returns undefined and reports fulfillment or rejection through that callback.
Expose accurate TypeScript overloadsdeclare-typescript-overloads
import { addCallback, type Callback } from 'promise-breaker';
export function sum(a: number, b: number): Promise<number>;
export function sum(a: number, b: number, done: Callback<number>): void;
export function sum(a: number, b: number, done?: Callback<number>) {
return addCallback(done, () => a + b);
}The explicit overloads make Promise and callback return types honest to callers; the implementation signature must accept both branches.
Preserve this when wrapping a methodpreserve-method-context
class Store {
prefix = 'user:';
load = promisify(function (id, done) {
backend.get(this.prefix + id, done);
});
}
const store = new Store();
const user = await store.load('42');The wrappers call the original function with the wrapper's this value. Extracting store.load into a bare variable still loses that context.
Alternatives
| Package | Registry | Pick it when |
|---|---|---|
| pify | npm | Use it when you only need to convert callback functions to Promise-returning functions and do not need a dual-mode public API |
| universalify | npm | Use it for focused fromCallback and fromPromise wrappers that expose callback-or-Promise calling conventions |
| thenify | npm | Use it when converting Node-style callback functions to Promises, including multi-argument callback results |