Jordan promises – async/await vs .then

As I’ve stated in a lot of other posts, I’m a big fan of async/await. I think it’s a pretty clean way to manage code your synchronous and asynchronous code.

Async/Await awesomeness

I want to compare some of the bad that can be avoided with async/await.

// Yucky then usage
async function getThePizza() {
	functionGettingFromMongo('someArguments').then((someData) => {
		
		// We want to use this data so we need to know that this promise completed
		doSomeWorkWithAPromise().then((moreData) => {
			const somethingCool = someData + moreData;

			insertToMongoNow(somethingCool).then((response) => {
				console.log('success!', response);
			}).catch((e) => {
				console.log('some error happened', e);
			});
		});

	});
}

So this is a function where we are entering callback hell. While .then() isn’t a callback function, codewise it reads the exact same. It’s a bunch of nested functions that get deeper and deeper and make code less and less readable.

// So good.
async function getThePizza() {
	const someData = await functionGettingFromMongo('someArguments');
	const moreData = await doSomeWorkWithAPromise();

	const somethingCool = someData + moreData;
	try {
		const response = await insertToMongoNow(somethingCool );
		console.log('success!', response);
	}
	catch(e) {
		console.log('some error happened', e);
	}
		
}

It’s a lot more readable. At the least it’s not getting more and more indented. I had been using async/await exclusively since I discovered it so much so that I started thinking of .then() as bad and kind of forgot that the beauty of programming is that almost everything is situational.

When .then() isn’t so bad

When I’m trying to scrape a lot of pages at once I don’t want to be blocked. I don’t use await but instead push the promises into an array and use Promise.all. That will ensure that I know when everything completes so I can close my db connection or puppeteer browser. Like this:

async function getThePizza() {
	
	const promises: any[] = [];
	for (let url of lotsAndLotsOfUrls) {
		promises.push(functionForHandlingTheUrl(url));
	}

	await Promise.all(promises);
	await closeMyDbConnectionOrPuppeteerOrSomething();
}

This works fine. Except for when I want to do something with the specific url after functionForHandlingTheUrl finishes with it. Now what? Using await looks like this:

async function getThePizza() {
	
	for (let url of lotsAndLotsOfUrls) {
		const something = await functionForHandlingTheUrl(url);
		await dbWorkOrSomething(something);
	}

	await closeMyDbConnectionOrPuppeteerOrSomething();
}

Very readable but much slower. I’m no longer taking advantage of any concurrency around I/O threads. So what’s the better alternative? This worked GREAT:

async function getThePizza() {
	
	const promises: any[] = [];
	for (let url of lotsAndLotsOfUrls) {
		promises.push(functionForHandlingTheUrl(url).then(something => {
			// This parts only happens when functionForHandlingTheUrl completes and doesn't block the rest
			return dbWorkOrSomething(something);
		}));

	}

	await Promise.all(promises);
	await closeMyDbConnectionOrPuppeteerOrSomething();
}

Using .then() here makes it so I have one nested function, it’s true, but we are able to do stuff when only that specific iteration is ready. It doesn’t block the rest of the looping so I am still lightning fast. And I am happy.

Update based on comment:

Thomas Grainger left a comment that talked about a way to do the same thing as the above code but stick to an async/await pattern rather than mix them. I tested it out and it really worked great. Here it is:

const lotsAndLotsOfUrls: any[] = [];

for (let i = 0; i < 10000; i++) {
	lotsAndLotsOfUrls.push(`${makeid(5)}---${i}`);
}

// Example of blocking, slower function
// (async () => {
// 	for (let i = 0; i < lotsAndLotsOfUrls.length; i++) {
// 		const something = await handleTheUrl(lotsAndLotsOfUrls[i]);
// 		await dbWorkOrSomething(something);
// 	}
// })();

// Example of non blocking, fast function without using then
const promises = lotsAndLotsOfUrls.map(async (url) => {
	const something = await handleTheUrl(url);
	await dbWorkOrSomething(something);
});

async function handleTheUrl(url: string): Promise<any> {
	return new Promise((resolve, reject) => {
		setTimeout(() => {
			resolve(url + ' ****handled');
		}, 1000 * Math.random());
	});
}

async function dbWorkOrSomething(something: string) {
	console.log('handling something with a db', something);
	return Promise.resolve();
}

function makeid(length: number) {
	let result = '';
	const characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
	const charactersLength = characters.length;
	for (let i = 0; i < length; i++) {
		result += characters.charAt(Math.floor(Math.random() * charactersLength));
	}
	return result;
}

Here’s a screenshot of the results. The above is with the non blocking function while the bottom has the slower, blocking function. Thanks Thomas!

Small PS. A lot of my featured images have been from Unsplash.com. It’s a REALLY great resource. Today’s is from Connor Jolley. Thanks Connor!

Looking for business leads?

Using the techniques talked about here at javascriptwebscrapingguy.com, we’ve been able to launch a way to access awesome business leads. Learn more at Cobalt Intelligence!

12 thoughts on “Jordan promises – async/await vs .then

  1. These are some of the common misconceptions of promises. You rarely need nesting. That’s the point of it; you can chain:

    function getThePizza() {
    return functionGettingFromMongo(‘someArguments’)
    .then((someData) => doSomeWorkWithAPromise()
    // Can avoid the nesting if the function takes someData
    .then(moreData => someData + moreData))
    .then(insertToMongoNow)
    .then(response => console.log(‘success!’, response))
    .catch(e => console.log(‘some error happened’, e));
    }

    You can chain actions before putting them into the Promise.all(), as well.

    function getThePizza() {
    return Promise.all(lotsAndLotsOfUrls.map(url =>
    functionForHandlingTheUrl(url).then(dbWorkOrSomething)
    )).then(closeMyDbConnectionOrPuppeteerOrSomething);
    };

    You can even use .map() to process your array of promises before handing off to .all(), if you prefer…

    function getThePizza() {
    return Promise.all(lotsAndLotsOfUrls
    .map(functionForHandlingTheUrl)
    .map(p => p.then(dbWorkOrSomething))
    )).then(closeMyDbConnectionOrPuppeteerOrSomething);
    };

  2. While I admire your work, there are some things which can be improved:

    1. Your first example of .then nesting is not idiomatic and specifically chosen to show it is inferior to async / await. See this article to see exactly what I am pointing at https://medium.com/@pyrolistical/how-to-get-out-of-promise-hell-8c20e0ab0513

    2. In the last example, you cannot have the keyword await inside a regular callback, the callback needs to have the async keyword. You can also fix the problem by removing the await keyword and simply returning dbWorkSomething().

  3. No need for .then here either, remember that for/push is a js antipattern. They’re really is no reason to mix .then/async await code

    const promises = lotsAndLotsOfUrls.map(async (url) => {
    const something = await functionForHandlingTheUrl(url);
    await dbWorkOrSomething(something);
    });

    1. Good call, man. I didn’t think of doing it that way. I went through and built a little test and it truly worked great.

      I added the chunk showing your code in action in the post.

      Thanks!

  4. Fyi: your initial code samples have no proper error handling which in the Promise case lead to unhandled rejections. Also some later examples have syntax errors for example using await in a non-async callback.

    1. Good call, Dave.

      I was aware of the lack of catches but didn’t worry about it since it wasn’t the point of the article. It’s probably not good to show these kinds of examples without proper error handling, though.

      I think I have the syntax errors fixed, now.

      Thanks!

Leave a Reply

Your email address will not be published. Required fields are marked *