Skip to main content
Ben Nadel at cf.Objective() 2014 (Bloomington, MN) with: Kitt Hodsden
Ben Nadel at cf.Objective() 2014 (Bloomington, MN) with: Kitt Hodsden ( @kitt )

Returning Promises From Async / Await Functions In JavaScript

By 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.

Want to use code from this post? Check out the license.

Reader Comments

1 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.

15,688 Comments

@Wiliam,

Thanks - glad this was interesting. It's funny, I love the Promise object, but it seems with all the "reactive programming", and RxJS stuff that is becoming more popular, people are starting to refer to "Promise Hell", the same way they referred to "Callback Hell" in the early Node.js days. But, I don't get it - for me, the Promise is such a nice construct. All the Reactive programming feels overly complicated for many cases. Yes, it's nice and elegant when it needs to be; but, the type of applications that I write just work really well with a simple, Promise-based workflow.

I believe in love. I believe in compassion. I believe in human rights. I believe that we can afford to give more of these gifts to the world around us because it costs us nothing to be decent and kind and understanding. And, I want you to know that when you land on this site, you are accepted for who you are, no matter how you identify, what truths you live, or whatever kind of goofy shit makes you feel alive! Rock on with your bad self!
Ben Nadel