Mastering Async JavaScript: From Callbacks to Promises and Beyond š
TOPICS
Callbacks
Promises
Async/Await
.then/.catch
finally
Why do we need promises in Javascript :
let's understand this first by some basic topics
Callbacks:
to understand promises we first need to understand callbacks. callbacks are functions that are passed as argument to another function and are executed after some operation has been completed.
example:
function fetchData(callback) {
setTimeout(() => { console.log("Data fetched!");
callback(); //after fetch data is executed then we call this
}, 1000); }
fetchData(() => { console.log("Callback executed!"); });
this is good for handling async calls ā» but can lead to a famous problem in Javascript called The Pyramid of doom ā also called callback hell
of deeply nested callbacks, let's see
example:
fetchData(() => {
fetchMoreData(() => {
fetchEvenMoreData(() => {
console.log("All data processed!");
});
});
});
Solutions to Avoid Callback Hell
To mitigate callback hell, developers often use:
Promises: These provide a cleaner way to handle asynchronous operations without nesting.
Async/Await: This syntax allows writing asynchronous code that looks synchronous, improving readability.
Let's understand Promise
States of a Promise
A promise can be in one of three states:
Pending: The initial state, meaning the promise is still being processed.
Fulfilled: The operation completed successfully, and the promise has a resulting value.
Rejected: The operation failed, and the promise has an error.
Using an analogy can make the concept of JavaScript promises much easier to understand. Letās use the online delivery analogy:
Online Delivery Analogy
Imagine you order a pizza online. Hereās how the process relates to a promise:
1. Ordering the Pizza (Creating a Promise)
When you place your order, you are essentially creating a promise. You tell the restaurant, "I want a pizza," and they promise to deliver it to you later.
- Pending: After you place your order, the restaurant is preparing your pizza. At this point, your order is in the pending state because you havenāt received it yet.
2. Waiting for Delivery (Pending State)
During this time, you might do other things while waiting for your pizza. The order is still being processed.
- You can think of this as the promise being in the pending stateāit's not fulfilled or rejected yet.
3. Pizza Delivered (Fulfilled State)
Once the pizza is ready and delivered to your door, you receive it and enjoy your meal. This is like the promise being fulfilled.
- You can now say, "The pizza has arrived!" This corresponds to calling
.then()
on your promise to handle the successful outcome.
4. Order Cancelled (Rejected State)
Now, imagine that after waiting for some time, you get a call from the restaurant saying they can't deliver your pizza because they ran out of ingredients.
- This is like the promise being rejected. You can handle this situation by calling
.catch()
to deal with the error.
Putting It Together
Hereās how it looks in terms of a promise:
Order Pizza: You create a promise.
Pending: The restaurant prepares your pizza.
Fulfilled: You receive your pizza and enjoy it (handled with
.then()
).Rejected: The restaurant informs you they canāt fulfill your order (handled with
.catch()
).
There can be only a single result or an error
The executor should call only one resolve
or one reject
. Any state change is final.
All further calls of resolve
and reject
are ignored:
let promise = new Promise(function(resolve, reject) {
resolve("doe");
reject(new Error("ā¦"));
// ignored
setTimeout(() => resolve("ā¦"));
// ignored}
);
The idea is that a job done by the executor may have only one result or an error.
Also, resolve
/reject
expect only one argument (or none) and will ignore additional arguments.
Great now lets understand how to consume Promise :
Consuming functions can be registered (subscribed) using the methods .then
and .catch
.
.then :
it can accept two arguments as shown below
promise.then(
function(result),
function(error)
)
lets see a successful response will look like when two arguments are passed to .then
const promise = new Promise(function(resolve,reject){
setTimeout(()=>resolve('done!'),1000)
})
promise.then(
result => alert(result), š //in success only this will run ignoring the second one
error => alert(error) ā //gets ignored
)
š In case of error only the second argument will run ignoring the first
result => alert(result)
catch
If weāre interested only in errors, then we can use null
as the first argument: .then(null, errorHandlingFunction)
. Or we can use .catch(errorHandlingFunction)
, which is exactly the same:
const promise = new Promise(function(resolve,reject){
setTimeout(()=>reject(new Error('error!')),1000)
})
// we can use something like
promise.then(
null,
error => alert(error) ā //gets ignored
)
// š” better and cleaner way to write the same is
promise.catch(error => alert(error))
// š” we can also write
promise
.then(resolve => alert(resolve))
.catch(error => alert(error))
The call .catch(f)
is a complete analog of .then(null, f)
, itās just a shorthand.
ahh finally , lets see what is finally
Cleanup: finally:
Just like thereās a finally
clause in a regular try {...} catch {...}
, thereās finally
in promises.
new Promise((resolve, reject)=>{
/*do something that takes time, and then call resolve or maybe reject */})
// runs when the promise is settled, doesn't matter successfully or not
.finally(()=> stop loading indicator)
// so the loading indicator is always stopped before we go on
.then(result=>show result,err=>show error)
The call .finally(f)
is similar to .then(f, f)
in the sense that f
runs always, when the promise is settled: be it resolve or reject. The idea of finally
is to set up a handler for performing cleanup/finalizing after the previous operations are complete. E.g. stopping loading indicators, closing no longer needed connections, etc.
To summarize:
A
finally
handler doesnāt get the outcome of the previous handler (it has no arguments). This outcome is passed through instead, to the next suitable handler.If a
finally
handler returns something, itās ignored.When
finally
throws an error, then the execution goes to the nearest error handler.
Summary of Topics:
Callbacks:
Functions passed as arguments to other functions, executed after a task is completed.
Leads to Callback Hell (deeply nested callbacks), making code hard to read and maintain.
Promises:
Provides a cleaner way to handle asynchronous tasks.
States:
Pending: Task in progress.
Fulfilled: Task completed successfully.
Rejected: Task failed.
.then / .catch:
.then(success, error)
: Handles successful and failed p
Subscribe to my newsletter
Read articles from Ayan Mehta directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by
Ayan Mehta
Ayan Mehta
Summiting code and mountains. web developer from the Himalayas. Expert in #GraphQL, #TypeScript, Gatsby, #Next.js, and CMS. Trek, snowboard, rock climb.