Mastering Async JavaScript: From Callbacks to Promises and Beyond šŸš€

Mastering Async JavaScript: From Callbacks to Promises and Beyond šŸš€

Ā·

5 min read

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:

  1. Pending: The initial state, meaning the promise is still being processed.

  2. Fulfilled: The operation completed successfully, and the promise has a resulting value.

  3. 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:

  1. Order Pizza: You create a promise.

  2. Pending: The restaurant prepares your pizza.

  3. Fulfilled: You receive your pizza and enjoy it (handled with .then()).

  4. 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:

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

  2. Promises:

    • Provides a cleaner way to handle asynchronous tasks.

    • States:

      • Pending: Task in progress.

      • Fulfilled: Task completed successfully.

      • Rejected: Task failed.

  3. .then / .catch:

    • .then(success, error): Handles successful and failed p
Ā