Skip to main content
Ben Nadel
On User Experience (UX) Design, JavaScript, ColdFusion, Node.js, Life, and Love.

Returning Promises From Async / Await Functions In JavaScript

By Ben Nadel on

Over the weekend, when I was using SessionStorage to cache form-data in Angular 9.1.9, I had a service object that included a number of async / await functions. And, as I was putting these functions together, my mind began to stumble over an inadequate portion of my Promise chain mental model. I was finding that returning a "raw value" from my async function was yielding the same result as returning a Promise from my async function. This sent my brain down a rabbit-hole, which I was thankfully able to come back from. But, as a means to flesh-out my wanting mental model, I'd like to take a quick look at returning Promise objects from async / await Functions in JavaScript (and TypeScript).

What seems to be your boggle? From the movie, Demolition Man.

First, let me demonstrate that returning a "raw" value and a Promise value from an async / await function both yield the same result. And, to do this, I'm going to use ts-node to run some TypeScript from my command-line:

async function getRawValue() : Promise<string> {

	return( "Raw value" );

}

async function getPromiseValue() : Promise<string> {

	return( Promise.resolve( "Promise value" ) );

}

// ----------------------------------------------------------------------------------- //
// ----------------------------------------------------------------------------------- //

console.log( "Testing Return Values:" );
console.log( "----------------------" );
getRawValue().then( console.log );
getPromiseValue().then( console.log );

As you can see, the first function returns a vanilla String value; and, the second function returns a Promise. And, when we run this TypeScript file through ts-node, we get the following terminal output:

bennadel$ npx ts-node ./demo-1.ts 
Testing Return Values:
----------------------
Raw value
Promise value

As you can see, both of these async functions return a Promise; and that Promise object resolves to the vanilla String values.

But, we've already identified the first flaw in my mental model. Which is that the above two async functions are different in some way. When, in reality, these two async functions are exactly the same because, according to the Mozilla Developer Network (MDN), any non-Promise return value is implicitly wrapped in a Promise.resolve() call:

The return value of an async function is implicitly wrapped in Promise.resolve - if it's not already a promise itself (as in this example).

As such, my return statement in the first function:

return( "Raw value" );

... is being implicitly re-written (so to speak) to this:

return( Promise.resolve( "Raw value" ) );

... which makes the two async functions exactly the same (semantically speaking).

As I was noodling on this concept, I came across the second flaw in my mental model, which is that I didn't have a solid understanding of how nested Promise objects would behave in an async function. To test this, I wrote another TypeScript file that returned Promise chains from the async functions:

async function getA() : Promise<string> {

	return( Promise.resolve( "Original value" ) );

}

async function getB() : Promise<string> {

	return( Promise.resolve( getA() ) ); // Promise.resolve( Promise )

}

async function getC() : Promise<string> {

	return( Promise.resolve( getB() ) ); // Promise.resolve( Promise( Promise ) )

}

async function getD() : Promise<string> {

	return( Promise.resolve( Promise.resolve( Promise.resolve( "Second value" ) ) ) );

}

// ----------------------------------------------------------------------------------- //
// ----------------------------------------------------------------------------------- //

console.log( "Testing Nested Resolve Values:" );
console.log( "------------------------------" );
getA().then( console.log );
getB().then( console.log );
getC().then( console.log );
getD().then( console.log );

In this demo, the first three functions work together to create a chain of Promise object. And, the fourth function just attempts to create a chain using nested Promise.resolve() calls. And, when we run the above TypeScript code, we get the following terminal output:

NOTE: I've altered the order of the output just to group like-named values. The actual timing puts "D" second.

bennadel$ npx ts-node ./demo-2.ts 
Testing Nested Resolve Values:
------------------------------
Original value
Original value
Original value
Second value

As you can see, regardless of the level of nesting of Promise calls, the async function eventually resolves to the underlying value.

Something about this just wasn't sitting right in my head. Since async / await functions are just syntactic sugar over regular Promise workflows, I decided to rewrite the above using the more verbose Promise syntax to see if I could connect the dots:

function getA() : Promise<string> {

	return( Promise.resolve( "Original value" ) );

}

function getB() : Promise<string> {

	return getA().then(
		( value ) => {

			return( value );

		}
	);

}

function getC() : Promise<string> {

	return getB().then(
		( value ) => {

			return( value );

		}
	);

}

// ----------------------------------------------------------------------------------- //
// ----------------------------------------------------------------------------------- //

console.log( "Testing Promise Chain:" );
console.log( "----------------------" );
getC().then( console.log );

As you can see, we've rewritten lines of code like:

return( Promise.resolve( getA() ) );

... to be this (abbreviated for this context):

return( getA().then( value => value ) );

... because, according to the Mozilla Developer Network (MDN), a Promise.resolve() call will "follow" any "thenable" object:

The Promise.resolve() method returns a Promise object that is resolved with a given value. If the value is a promise, that promise is returned; if the value is a thenable (i.e. has a "then" method), the returned promise will "follow" that thenable, adopting its eventual state; otherwise the returned promise will be fulfilled with the value. This function flattens nested layers of promise-like objects (e.g. a promise that resolves to a promise that resolves to something) into a single layer.

Now, if we run this TypeScript code through ts-node, we get the following terminal output:

bennadel$ npx ts-node ./demo-3.ts 
Testing Promise Chain:
----------------------
Original value

When I saw the async / await functions "de-sugared" down into their underlying Promise-based implementation, it finally clicked in my head! Of course all the Promise chains flatten-down in an async function - that's what Promise chains do!

The beauty of the Promise chain is that it allows for asynchronous branching. That's what makes Promise chains so darn powerful. Heck, we can even create asynchronous, recursive Promise chains. And, this all works because the .then() call ultimately flattens the chain, resulting in the last resolved value.

I love Promise objects. They are the bee's knees. And, I love the ease and simplicity of the async / await control flow in modern JavaScript and TypeScript. But, I was having trouble porting my older Promise mental model over to the newer async syntax. Seeing the two syntaxes side-by-side finally made it click.



Reader Comments

Nice post, that's a thing I have saw since some time ago when I tried to write an async function that returns a promise, both got resolved!

I was searching the nice way to get the raw of the first promise.

Reply to this Comment

Post A Comment

You — Get Out Of My Dreams, Get Into My Blog
Live in the Now
Oops!
NEW: Some basic markdown formatting is now supported: bold, italic, blockquotes, lists, fenced code-blocks. Read more about markdown syntax »
Comment Etiquette: Please do not post spam. Please keep the comments on-topic. Please do not post unrelated questions or large chunks of code. And, above all, please be nice to each other - we're trying to have a good conversation here.