Disclaimer: Feedback from some people who have read this is that this approach is not suitable for production - I tend to agree with this. It is rather an idea on how to use async/await less than just another promises tutorial or lifehack. If you like such ideas, happy reading.

In node.js asynchronous operations notify about completion by calling callback function. Usually the first argument of this function is error and the rest (most of the callbacks has two arguments) is a function-specific data. The signature of the callback function is (Error, any) => void.

// callback for the file reading operation
var print = (err, data) => console.log(data);

require('fs').readFile('text.txt', {encoding:'utf8'}, print);

Although it became amazingly popular, it was frequently discovered that using this approach could lead to a maintainability issue, called "callback hell". Modularise callback chain is not as intuitive as using variables for results so when several callbacks become nested the famous callback pyramid appears:

req.get('user', (err, to) => {
  db.getOne('select msg from ...', (err, msg) => {
    log.info(`${to}: ${msg}`, err => {
      api.send(to, msg, err => {
        // ...
      });
    });
  });
});

There were several approaches to this and now the community consensus seems to be using the promises. The promises approach linearises branches of the callback tree. See how example below changes when log.info becomes a promise:

req.get('user', (err, to) => {
  db.getOne('select msg from ...', (err, msg) => {
    log.info(`${to}: ${msg}`)
      .then(() => api.send(to, msg, err => {
        // ...
      }));
  });
});

Now when the api.send becomes a promise:

req.get('user', (err, to) => {
  db.getOne('select msg from ...', (err, msg) => {
    log.info(`${to}: ${msg}`)
      .then(() => api.send(to, msg))
      .then(() => { /* ... */ });
  });
});

However, when db.getOne becomes a promise, the code becomes a bit more complicated than expected:

req.get('user', (err, to) => {
  db.getOne('select msg from ...')
    .then(msg => log.info(`${to}: ${msg}`).then(() => msg))
    .then(msg => api.send(to, msg))
    .then(() => { /* ... */ });

At then even more complicated when the req.get becomes a promise:

req.get('user')
  .then(to => db.getOne('select msg from ...').then(msg => ({ to, msg })))
  .then(d => log.info(`${d.to}: ${d.msg}`).then(() => d))
  .then(d => api.send(d.to, d.msg))
  .then(() => { /* ... */ });

The async/await pattern was implemented in JavaScript with intention to address issues with callbacks and then-chains and was definitely a great improvement.

let to = await req.get('user');
let msg = await db.getOne('select msg from ...');
await log.info(`${to}: ${msg}`);
await api.send(to, msg);
// ...

Although this approach slowly supplants the callback approach, it still an attempt to solve the problem that should not exist at the first place.

By the way, the last example is incorrect as it awaits the gets one by one. The better code is following:

let [ to, msg ] = await Promise.all([
  req.get('user'),
  db.getOne('select msg from ...')
]);
await Promise.all([
  log.info(`${to}: ${msg}`),
  api.send(to, msg)
]);
// ...

But the code that even better is a code without async/await at all. Just like the following:

let to = req.get('user');
let msg = db.getOne('select msg from ...');
log.info(`${to}: ${msg}`);
api.send(to, msg);
// ...

Of course, the code on the listing above is asynchronous and non-blocking. So how could it be implemented? Well, although it is not possible to remove the second Promise.all from the correct example, the first one can be removed. The functions log.info and api.sent just should accept promises to make this possible. Such change is trivial, for example the send could be wrapped just like the following:

// part of the api class

 * @param to string|Promise<string>
 * @param msg string|Promise<string>
 */
send : async (to, msg) => {
  const [ _to, _msg ] = await Promise.all([ Promise.resolve(to), Promise.resolve(msg) ]);
  // call original send with string parameters
  return this._send(_to, _msg);
},

If you like this approach, you can try the supro (SUper PROmises) library. It helps converting the existing functions with plain values so they accept promises:

const fs = require('fs').promises,
  pkg = require('my-package');

const [ readData1, readData2, report, writeFile ] =
  require('supro').up(pkg.readData1, pkg.readData2, pkg.report, fs.writeFile);

async writeFile('name', report(readData1(), readData2()));