Ways to use promises - Part 1
_Promise is a fundamental concept of JS (and life). The next quote summarizes it well
"A
Promiseis 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
asyncandawaitkeywords 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
pendingstate. asyncalways return apromiseimplicitly.awaitis 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
awaitthem usingPromise.all. - The
for awaitstatement 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...ofloop 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@@asyncIteratorbut 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.
