Returning Promises From Async / Await Functions In JavaScript
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).
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 inPromise.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 aPromise
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
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.
@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, thePromise
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.