Callbacks are significantly faster than Promise
s in Node.js. How can we get the performance of callbacks while benefiting from the Promise
and async/await syntax?
JavaScript could introduce CallbackAwaitExpression, which syntactically would look very much like the existing AwaitExpression but it would operate on top of callbacks instead of promises.
The callbacks would have the following form:
type Callback<V, E = unknown> =
| (error: E) => void;
| (error: void, value: V) => void;
The new CallbackAwaitExpression would have an extra identifier parameter of type Callback
, syntactically in between the await
keyword and the expression being awaited, for example, note the cb
identifier:
await cb fs.readFile('myfile.txt', 'utf-8', cb);
Likewise, the async function syntax would also be extended to allow AsyncCallbackFunction type. There as well, the syntax would allow a single callback identifier:
async cb function(args, cb) {
// ...
}
Putting this all together, this would allow to write async/await syntax-powered code, while benefiting from the performance of callbacks. It would allow to write code like this:
async _ function getFileData(filename, _) {
try {
const data = await _ fs.readFile('myfile.txt', 'utf-8', _);
return 'mydata: ' + data;
} catch (error) {
if (!!error && typeof error === 'object' && error.code === 'ENOENT') {
throw new Error('not found');
}
throw error;
}
}
The code would be equivalent to the existing JavaScript:
function getFileData(filename, callback) {
const onCatch = (error) => {
if (!!error && typeof error === 'object' && error.code === 'ENOENT') {
callback(new Error('not found'));
} else {
callback(error);
}
};
try {
fs.readFile('myfile.txt', 'utf-8', (err, data) => {
if (err) {
onCatch(err);
} else {
try {
callback(null, 'mydata: ' + data);
} catch (error) {
onCatch(error);
}
}
});
} catch (error) {
onCatch(error);
}
}
Here is how the above code can look like using the existing async/await syntax. The code is almost equivalent to the async/await callback proposal, but less performant due to Promise
usage.
async function getFileDataAsync(filename) {
try {
const data = await promisify(fs.readFile)('myfile.txt', 'utf-8');
return 'mydata: ' + data;
} catch (error) {
if (!!error && typeof error === 'object' && error.code === 'ENOENT') {
throw new Error('not found');
}
throw error;
}
}
Or, equivalently, the good old promisify
utility could convert our callback powered function to this Promise
powered one:
const getFileDataAsync = promisify(getFileData);
Sugar Syntax
When awaiting one can explicitly specify the callback identifier and explicitly use it as an argument:
await myCallback fs.readFile('a.txt', myCallback)
If the await identifier myCallback
is not used, it is automatically inserted in the function call as the last argument:
await _ fs.readFile('a.txt')
When defining an async callback function, the above text proposed to explicitly specify the callback identifier, like so:
async myCallback function getData(filename, myCallback) {}
Instead it can be reduced by simply using the async
keyword once, in place of some argument:
function getData(filename, async) {}
The above syntax modifications reduce the example getFileData
function to the following:
function getFileData(filename, async) {
try {
const data = await _ fs.readFile('myfile.txt', 'utf-8');
return 'mydata: ' + data;
} catch (error) {
// ...
}
}
Using with TypeScript
From the point of view of TypeScript, to define a new async callback function, one can simply use the CallbackType
:
type GetFileData = (filename: string, callback: Callback<string>) => void;
Alternatively, the async
type shorthand could be introduced, to make it more explicit:
type GetFileData = (filename: string, async<string>) => void;
Maybe it could be called callback
:
type GetFileData = (filename: string, callback<string>) => void;
Top comments (3)
Putting it all together, awaiting an async callback IIFE:
equivalent to the full syntax:
formatted sugar syntax:
with the sugar syntax the
getFileData
in the post becomes:Alternative syntax for async function definition could be to place the
async
keyword in the place of the callback function:instead of:
arrow syntax:
Syntactic sugar: when the await callback is not used in the expression, it is automatically inserted as the last function argument: