Ways to use promises - Part 1
_Promise is a fundamental concept of JS (and life). The next quote summarizes it well
"A
Promise
is a proxy for a value not necessarily known when the promise is created. It allows you to associate handlers with an asynchronous action's eventual success value or failure reason."
Usually, the use of promises is implicit and happens under the hood. I am going to show pitfalls and ways to use it directly or indirectly. This part will cover:
- async/await
- Generator
- AsyncGenerator
async/await
Let's start with the basics:
The
async
andawait
keywords enable asynchronous, promise-based behavior to be written in a cleaner style, avoiding the need to explicitly configure promise chains.
With promises, we write:
function fooPromise() {
return new Promise((resolve) => resolve(1));
}
And the equivalent async
function is:
async function fooAsync() {
return 1;
}
The results of those functions are the same:
console.log(fooAsync()); // Promise { 1 }
console.log(fooPromise()); // Promise { 1 }
fooPromise().then((result) => console.log(result)); // 1
fooAsync().then((result) => console.log(result)); // 1
console.log(await fooAsync()); // 1
console.log(await fooPromise()); // 1
Some interesting points we see here:
- The promises results were evaluated immediately without passing through the
pending
state. async
always return apromise
implicitly.await
is done on promises, it gives the result when it is ready.
What do you say we complicate it a bit? š«£
async function fooAsyncAwait() {
return await new Promise((resolve) => resolve(1));
}
async function fooAsyncPromise() {
return new Promise((resolve) => resolve(1));
}
console.log(fooAsyncAwait()); // Promise { <pending> }
console.log(fooAsyncPromise()); // Promise { <pending> }
console.log(await fooAsyncAwait()); // 1
console.log(await fooAsyncPromise()); // 1
In one function we have await
and in the other, we don't. Intuitively-
await fooAsyncPromise()
should have returned a promise, but JS defines a special
case for async
function that returns a promise.
"Async functions always return a promise. If the return value of an async function is not explicitly a promise, it will be implicitly wrapped in a promise."
Moreover, we can see that the promise is at pending
state and can't be
evaluated immediately as before, this is because of how the event-loop work, but
that's for another post.
You can await
not only promises:
function foo() {
return 1;
}
console.log(await foo()); // 1
console.log(foo()); // 1
It can be helpful if you don't know if the value is a promise, or not. You can
just await
it without doing any extra checks.
Generator
In short, it can be described as a way to iterate values one by one instead of storing them in memory.
For example:
function* fooGenerator() {
yield "a";
yield "b";
yield "c";
}
const generator = fooGenerator();
console.log(...generator); // a b c
console.log(...generator); //
You can read more in the MDN docs.
Generators alone are not using promises, but using them in generators is almost natural in many cases. Let's see some code! š¾
function* fooGenerator() {
yield Promise.resolve("a");
yield Promise.resolve("b");
yield Promise.resolve("c");
}
console.log(fooGenerator()); // Object [Generator] {}
console.log(...fooGenerator()); // Promise Promise Promise
console.log(await Promise.all(fooGenerator())); // [ 'a', 'b', 'c' ]
for (const x of fooGenerator()) console.log(await x); // a b c
for await (const x of fooGenerator()) console.log(x); // a b c
What can we see here?
- Remember that
fooGenerator()
is a generator of promises, we'll get back to it later. - You can spread the generator, which gives a list of promises, so we can
await
them usingPromise.all
. - The
for await
statement makes an iterable of promises to be sequential. The last two lines are equivalent in that matter.
AsyncGenerator
The difference between Generator
and AsyncGenerator
can be seen as the
difference between a function
and async function
. The value in the yield
statement will be wrapped by promise.
BUT, unlike Generator
, the AsyncGenerator
can be accessed only by
for await
or yield*
. Array/parameter spreading and destruction will not work!
The
for await...of
loop andyield*
in async generator functions (but not sync generator functions) are the only ways to interact with async iterables. Usingfor...of
, array spreading, etc. on an async iterable that's not also a sync iterable (i.e. it has@@asyncIterator
but no@@iterator
) will throw a TypeError: x is not iterable.
Let's take the last example and change the function*
to an async function*
.
async function* fooAsyncGenerator() {
yield "a";
yield "b";
yield "c";
}
console.log(fooAsyncGenerator()); // Object [AsyncGenerator] {}
console.log(...fooAsyncGenerator()); // Uncaught TypeError: Found non-callable @@iterator
for await (const x of fooAsyncGenerator()) console.log(x); // a b c
There is a very interesting
proposal for adding
helper functions for the Iterator
and AsyncIterator
objects. I would love to
see that in action!
Conclusion
Promises exist everywhere in JS, directly or indirectly. And you can elaborate many of the JS functionality to easily manage concurrency.
Even with the strong functionality that exists right now, the future of JS is full of additional functionality and optimizations in this subject.
Hope you learned something new about promises, continue reading in part 2.