Introduction
When using await with a function that accepts a callback-style argument, you often need to pass additional fixed parameters to that callback. In JavaScript, the standard techniques are closures, Function.prototype.bind(), arrow function wrappers, and partial application. The most common pattern is wrapping the callback in an arrow function that captures the fixed parameter from the enclosing scope. This works seamlessly with await because await simply resolves a Promise — how the Promise's internal callback receives parameters is a separate concern.
The Problem: Passing Fixed Values to Callbacks
1// You have an async function that takes a callback
2function fetchData(url, callback) {
3 // ... fetches data and calls callback(error, result)
4}
5
6// You want to call it with await and pass a fixed "format" parameter
7// But the callback signature is fixed: (error, result)
8
9// How do you get "format" into the callback?
Solution 1: Arrow Function Closure
1const format = 'json';
2
3// Wrap the callback-based function in a Promise
4function fetchFormatted(url, format) {
5 return new Promise((resolve, reject) => {
6 fetchData(url, (error, result) => {
7 if (error) return reject(error);
8 // 'format' is captured from the closure
9 resolve(processResult(result, format));
10 });
11 });
12}
13
14const data = await fetchFormatted('/api/users', 'json');
The arrow function (error, result) => { ... } closes over format from the outer scope. This is the most common and readable approach.
Solution 2: Using bind()
1function handleResult(format, error, result) {
2 if (error) throw error;
3 return processResult(result, format);
4}
5
6// bind prepends 'json' as the first argument
7const boundHandler = handleResult.bind(null, 'json');
8// boundHandler(error, result) → handleResult('json', error, result)
9
10function fetchWithBind(url, format) {
11 return new Promise((resolve, reject) => {
12 fetchData(url, (error, result) => {
13 try {
14 resolve(handleResult.bind(null, format)(error, result));
15 } catch (e) {
16 reject(e);
17 }
18 });
19 });
20}
21
22const data = await fetchWithBind('/api/users', 'json');
bind() creates a new function with preset arguments. The fixed parameter comes first, followed by the callback's own arguments.
Solution 3: Promisifying with Fixed Parameters
1import { promisify } from 'util';
2
3// Original callback-based function
4function readFile(path, encoding, callback) {
5 // callback(error, data)
6}
7
8// Promisify it
9const readFileAsync = promisify(readFile);
10
11// Now pass fixed parameters directly — await handles the rest
12const content = await readFileAsync('/path/to/file', 'utf-8');
13
14// For custom promisification with fixed parameters
15function fetchWithOptions(url, options, callback) {
16 // callback(error, result)
17}
18
19const fetchAsync = promisify(fetchWithOptions);
20
21// Fixed parameters go naturally before the (now-removed) callback
22const result = await fetchAsync('/api/data', { format: 'json', timeout: 5000 });
util.promisify removes the callback parameter and returns a Promise-based function. Fixed parameters are passed in their original positions.
Solution 4: Partial Application Helper
1// Generic partial application
2function partial(fn, ...fixedArgs) {
3 return (...remainingArgs) => fn(...fixedArgs, ...remainingArgs);
4}
5
6// Usage
7function processItem(format, retryCount, item) {
8 return { ...item, format, retryCount };
9}
10
11const processAsJson = partial(processItem, 'json', 3);
12// processAsJson(item) → processItem('json', 3, item)
13
14// With async/await in a loop
15const items = await fetchAllItems();
16const processed = items.map(processAsJson);
1// Partial application with named parameters (more readable)
2function createProcessor(options) {
3 return async function (item) {
4 const result = await transform(item, options.format);
5 if (options.validate) {
6 await validate(result);
7 }
8 return result;
9 };
10}
11
12const processJson = createProcessor({ format: 'json', validate: true });
13const result = await processJson(rawItem);
Real-World Example: Event Handlers with Fixed Context
1class DataProcessor {
2 constructor(apiBase) {
3 this.apiBase = apiBase;
4 }
5
6 async processAll(ids, format) {
7 // Fixed parameter 'format' passed to each async callback
8 const results = await Promise.all(
9 ids.map(id => this.processOne(id, format))
10 );
11 return results;
12 }
13
14 async processOne(id, format) {
15 const response = await fetch(`${this.apiBase}/items/${id}`);
16 const data = await response.json();
17
18 // 'format' is available via closure
19 return format === 'csv' ? toCSV(data) : data;
20 }
21}
22
23const processor = new DataProcessor('https://api.example.com');
24const results = await processor.processAll([1, 2, 3], 'csv');
TypeScript: Typed Fixed Parameters
1// Type-safe partial application
2function withFormat<T>(
3 fn: (format: string, item: T) => Promise<string>,
4 format: string
5): (item: T) => Promise<string> {
6 return (item: T) => fn(format, item);
7}
8
9async function serialize(format: string, data: object): Promise<string> {
10 if (format === 'json') return JSON.stringify(data);
11 if (format === 'yaml') return toYAML(data);
12 throw new Error(`Unknown format: ${format}`);
13}
14
15const toJson = withFormat(serialize, 'json');
16const result = await toJson({ name: 'Alice' });
17// result: '{"name":"Alice"}'
Common Pitfalls
Losing this context when using bind: bind() sets both this and fixed arguments. If you only want to fix arguments, pass null as the first argument to bind. But in class methods, you may need to pass the correct this or use arrow functions instead.
Stale closures in loops: When using closures inside for loops with var, all callbacks capture the same variable reference. Use let, const, or for...of to create a new binding per iteration, or pass the value through bind.
Confusing argument order with bind: bind prepends arguments. If the callback signature is (error, result) and you bind a format parameter, the callback becomes (format, error, result). Make sure the receiving function expects the fixed parameter first.
Promisifying functions that do not follow the Node callback convention: util.promisify assumes the last parameter is a callback of the form (error, result). Functions with different signatures need custom promisification using util.promisify.custom or a manual Promise wrapper.
Creating unnecessary closures in hot paths: Each arrow function creates a new closure object. In performance-critical loops processing millions of items, pre-bind the function outside the loop or use a shared callback to avoid garbage collection pressure.
Summary
Use arrow function closures to capture fixed parameters — the simplest and most readable approach
Use Function.prototype.bind(null, fixedArg) to prepend fixed arguments to any function
Use util.promisify to convert callback-based Node.js functions into Promise-based ones with natural parameter passing
Create partial application helpers for reusable parameter binding across multiple calls
In TypeScript, type the partial application wrapper to maintain type safety
Prefer closures over bind for readability, and bind over closures for reusable function references