ezik
ezik

Reputation: 482

How to avoid using 'callback hell' and turn to Promises?

I have two functions:

  1. Uploads files to server (unknown time)
  2. Launches ssh command on server (pretty much 0.1 seconds)

To solve this problem I used callbacks like so:

const fs = require("fs");
const sftp = require("./_uploader/sftp");
const pm2 = require("./_uploader/pm2");

const credentials = {
  host: "",
  port: 123,
  username: "",
  key: "key",
};

sftp(credentials, () => {
  pm2(credentials, () => {
    console.log("done");
  });
});

sftp

module.exports = ({ host, port, username, key }, cb) => {
  ...
  cb('done')
}

pm2

module.exports = ({ host, port, username, key }, cb) => {
  ...
  cb('done')
}

I know it can be done with the help of Promises or async functions, but all my attempts were unsuccessful. How should it be done properly?

Upvotes: 0

Views: 836

Answers (2)

Zac Delventhal
Zac Delventhal

Reputation: 4010

Let's forget about async functions for a moment. They are just a bit of syntactic sugar on top of Promises. If you don't already have Promises, then they won't do you any good.

Think of Promises as wrapper objects that handle callbacks for you. They really aren't much more than that. When we construct a new Promise, we get passed special resolve and reject functions. We can then use one or both of these functions in place of traditional callbacks. So for example, if we wanted to promisify setTimeout:

const timeoutAsPromised = (delay) => {
  return new Promise((resolve) => {
    setTimeout(resolve, delay);
  });
};

We return a Promises immediately. That's important. Our function has to give the Promise back now so it can be used right away. But in place of a callback, we use the resolve function that the Promise constructor gave us. Now we can call it like this:

timeoutAsPromised(1000)
  .then(() => console.log('One second has passed!'));

For your use case, we can do much the same thing. Just take your functions and wrap them in a promisified version:

const sftpAsPromised = (credentials) => {
  return new Promise((resolve) => {
    sftp(credentials, resolve);
  });
};

Though depending on who wrote sftp and how, it might be just as easy to rewrite it from the ground up to return a Promise instead of taking a callback:

module.exports = ({ host, port, username, key }) => {
  return new Promise((resolve) => {
    ...
    resolve('done')
  });
};

And heck, if you have a lot of these asynchronous functions, and they all have the same function signature (they take one argument and then a callback), you might even write a little promisify utility to handle them all:

const promisify = (fn) => (arg) => {
  return new Promise((resolve) => {
    fn(arg, resolve);
  });
};

const pm2AsPromised = promisify(pm2);

Okay! Now let's talk about async/await briefly. These are a lovely bit of syntax, but it is important to always remember that they only work on Promises. If you have some asynchronous functions built on callbacks, async/await is useless to you.

Thankfully we just did that work promisifying our callback functions. So lets make a wrapping async function, and then await our promisified function calls. You can think of await as basically just replacing the .then method.

const handlerServer = async () => {
  await sftpAsPromised(credentials);
  await pm2AsPromised(credentials);
};

handleServer();

Hopefully that clears things up!

Upvotes: 0

daddygames
daddygames

Reputation: 1928

There are different ways to do this. I won't cover them all.

OPTION 1: AWAIT PROMISE

For this example you will want to run the code inside an async function to take advantage of the await keyword.

Update your SMTP and PM2 functions to return a Promise. Inside the promise will handle the logic and resolve("done") releases the promise so that code can move on.

module.exports = ({ host, port, username, key }) => {
  return new Promise((resolve) => {
        ...
        resolve("done");
    }); 
}

Now you can update the execution code to take advantage of the promises:

const fs = require("fs");
const sftp = require("./_uploader/sftp");
const pm2 = require("./_uploader/pm2");

const credentials = {
  host: "",
  port: 123,
  username: "",
  key: "key",
};


const runApp = async () => {
    await sftp(credentials);
    await pm2(credentials);
    console.log("done");
}

runApp();

OPTION 2: CHAIN PROMISES

Another way to do this is by chaining the Promises. I opt not to do this very often because it can become a mess of nested logic.

const fs = require("fs");
const sftp = require("./_uploader/sftp");
const pm2 = require("./_uploader/pm2");

const credentials = {
  host: "",
  port: 123,
  username: "",
  key: "key",
};


sftp(credentials).then(result1 => {
    pm2(credentials).then(result2 => {
        console.log("done");    
    });
});

OPTION 3: PROMISE ALL

Another option is to use Promise.all

const fs = require("fs");
const sftp = require("./_uploader/sftp");
const pm2 = require("./_uploader/pm2");

const credentials = {
  host: "",
  port: 123,
  username: "",
  key: "key",
};


Promise.all([
    sftp(credentials),
    pm2(credentials)
]).then(result => {
    console.log("done");    
});

Upvotes: 1

Related Questions