Memoize API Calls
intermediateCache in-flight and resolved requests to deduplicate API calls — with cache invalidation trade-offs.
Implement util.promisify in JavaScript: error-first callback conventions, this preservation, multi-argument results, and the settle-once guarantee against misbehaving callbacks.
Implement
promisify(fn): given a function whose last argument is an error-first callback —fn(...args, (err, result) => {})— return a function that takes the same leading arguments and returns a promise.
This is Node's util.promisify, and the question tests whether you understand the convention being bridged: callback APIs signal errors as the callback's first argument ((err, result)), promises signal them as rejections. The implementation is a mechanical translation of that convention — plus the defensive details that separate a demo from a library.
function promisify(fn) {
return function (...args) {
return new Promise((resolve, reject) => {
let settledByCallback = false;
const callback = (err, ...results) => {
settledByCallback = true;
if (err != null) {
reject(err);
} else {
// one result → unwrap it; several → keep the array
resolve(results.length <= 1 ? results[0] : results);
}
// extra invocations hit a settled promise — harmless by design
};
try {
fn.call(this, ...args, callback); // preserve `this` for method usage
} catch (err) {
// a SYNC throw before the callback: reject rather than explode
if (!settledByCallback) reject(err);
}
});
};
}const readFileAsync = promisify(fs.readFile);
const content = await readFileAsync("config.json", "utf8");
// err path: readFileAsync("missing.txt") → rejected promise → try/catch it
// `this` preservation matters for method-style APIs:
const db = {
connectionString: "...",
query(sql, cb) { /* uses this.connectionString */ },
};
db.queryAsync = promisify(db.query);
await db.queryAsync("SELECT 1"); // works because of fn.call(this, ...)(err, result) in 2009 so every async API had one error contract; promisify works because that contract is universal. Callback APIs that don't follow it (see below) can't be promisified generically.err != null, not if (err) — a callback reporting 0 or "" as its "error" would be treated as success by a truthiness check. Rare, but the != null-check costs nothing and is what Node does.(err, lat, lon) doesn't fit a single resolution value. Unwrapping singles and arraying multiples is a policy; Node's real promisify instead relies on a customPromisifyArgs symbol for named multi-results. Stating "this is a convention decision, here's mine, here's Node's" is exactly the right register.fn may validate arguments and throw before ever calling the callback; without the try/catch, that error escapes the executor's caller in a confusing way. (Inside new Promise, a sync throw actually rejects — but only if it happens inside the executor; the guard also documents intent.).then is always a microtask): the Zalgo-proofing argument, one sentence.fn calls back with only (err) — success with no value → resolve undefined (e.g. fs.unlink); the results.length <= 1 branch covers it.setTimeout(cb, ms) (no error slot, callback first among trailing args) or xhr.onload/onerror (two callbacks) don't fit; each needs a hand-written wrapper. Recognizing when promisify doesn't apply is part of the question.this — promisify(db.query) detaches the method; either promisify(db.query.bind(db)) or the fn.call(this, ...) + assign-to-object pattern above. Ties directly into the this keyword and custom bind.if (err) truthiness (falsy error values misread as success).this (fn(...args, cb) instead of fn.call(this, ...)).results always as an array — makes the common single-value case awkward ((await f())[0]).callbackify(asyncFn)." — (...args, cb) => asyncFn(...args).then((r) => cb(null, r), (e) => cb(e ?? new Error("falsy rejection"))) — note the falsy-rejection wrinkle: reject(undefined) must still produce a truthy err.success event, reject on error, remove both listeners on settle — the leak-free version is the senior answer; see event emitter.promisify.custom do?" — lets an API ship its own promise version behind a symbol; promisify returns it verbatim. Exists precisely because generic translation can't fit every API.new Promise ad hoc?" — you can, but N call sites re-implement the convention N times with N chances at the if (err) bug; promisify centralizes it. The DRY argument, made concrete.FileReader, IndexedDB — see the deferred pattern in Promise.withResolvers), and every SDK stuck on callbacks.Cache in-flight and resolved requests to deduplicate API calls — with cache invalidation trade-offs.
The three concurrency shapes in one guide — sum vs slowest vs fastest, what 'parallel' means on one thread, and first-settle vs first-success.
Process a large list of async tasks in fixed-size sequential batches to protect downstream services.
Retry a failing async operation N times with backoff — a small function with big production implications.