Learn-Full-Stack / Javascript

0 stars 0 forks source link

Day-3 Promise , callback , try catch , async and await #6

Open purushothaman-source opened 1 year ago

purushothaman-source commented 1 year ago

Callback

promises

Async/Await

Cheat sheet

purushothaman-source commented 1 year ago

CALLBACK

Remove negative Numbers from list

<!DOCTYPE html>
<html>
<body style="text-align: right">

<h1>JavaScript Functions</h1>
<h2>Callback Functions</h2>
<p id="demo"></p>

<script>
// Create an Array
const myNumbers = [4, 1, -20, -7, 5, 9, -6];

// Call removeNeg with a Callback
const posNumbers = removeNeg(myNumbers, (x) => x >= 0);

// Display Result
document.getElementById("demo").innerHTML = posNumbers;

// Remove negative numbers
function removeNeg(numbers, callback) {
  const myArray = [];
  for (const x of numbers) {
    if (callback(x)) {
      myArray.push(x);
    }
  }
  return myArray;
}
</script>

</body>
</html>
purushothaman-source commented 1 year ago

When to Use a Callback?

The examples above are not very exciting.

They are simplified to teach you the callback syntax.

Where callbacks really shine are in asynchronous functions, where one function has to wait for another function (like waiting for a file to load).

purushothaman-source commented 1 year ago

Clock

<!DOCTYPE html>
<html>
<body>

<h1>JavaScript Functions</h1>
<h2>setInterval() with a Callback</h2>

<p>Using setInterval() to display the time every second (1000 milliseconds).</p>

<h1 id="demo"></h1>

<script>
setInterval(myFunction, 1000);

function myFunction() {
  let d = new Date();
  document.getElementById("demo").innerHTML=
  d.getHours() + ":" +
  d.getMinutes() + ":" +
  d.getSeconds();
}
</script>

</body>
</html>
purushothaman-source commented 1 year ago

Callback Alternatives

With asynchronous programming, JavaScript programs can start long-running tasks, and continue running other tasks in parallell.

But, asynchronus programmes are difficult to write and difficult to debug.

Because of this, most modern asynchronous JavaScript methods don't use callbacks. Instead, in JavaScript, asynchronous programming is solved using Promises instead.

purushothaman-source commented 1 year ago

What Is A Promise

A promise in JavaScript is very similar to a promise in real life. Imagine that you loan your friend Jim $100 to buy something. When you loan him the money he promises you that he will pay you back in full after work the next day. You now have Jim's promise that as long as nothing bad happens he will pay you back in the future after he finishes work. If something unexpected happens, though, and Jim breaks his leg at work then of course he will not be able to fulfill his promise and you will not get paid back on time.

This is exactly how promises are handled in JavaScript. A promise is just some set of code that says you are waiting for some action to be taken (Jim finishing work) and then once that action is complete you will get some result (Jim paying you back). Sometimes, though, a promise will be unfulfilled (Jim breaking his leg) and you will not get the result you expect and instead will receive a failure/error.

If you are familiar with callbacks then you may realize that promises solve a similar problem to callbacks, but they do so in a much more elegant way.

purushothaman-source commented 1 year ago

Implementing Promises

function handleJimWork(successCallback, errorCallback) {
   // Slow method that runs in the background
  const success = doJimWork()
  if (success) {
    successCallback()
  } else {
    errorCallback()
  }
}

handleJimWork(() => {
  console.log('Success')
}, () => {
  console.error("Error")
})

In this example we have a handleJimWork function that takes in a callback for what to do on success and failure. We then run the doJimWork function which is a slow function that runs in the background. This would be similar to doing something like a fetch request to get information from a server. Then based upon the result of running this slow background function we get a result of either true or false depending on if Jim was able to successfully get through the work day. Depending on that value we either call the success or error callback. Then when we call handleJimWork we pass in both a success and error function which will run depending on the success of doJimWork.

Now let's look at how we convert this to use promises.

function handleJimWork() {
  return new Promise((resolve, reject) => {
    // Slow method that runs in the background
    const success = doJimWork()
    if (success) {
      resolve()
    } else {
      reject()
    }
  })
}

const promise = handleJimWork()
promise.then(() => {
  console.log('Success')
}).catch(() => {
  console.error("Error")
})

You will immediately notice that the code is very similar, but with one big change. Instead of passing callbacks to handleJimWork we instead are using the reject, and resolve methods of a promise. We are also returning a promise from handleJimWork and then when we call handleJimWork we are using that promise by calling the .then and .catch methods on the promise.

First, lets start by breaking down what is happening in handleJimWork. If you want to convert a function to use promises you need to always return a promise from that function since you need access to a promise object to check if the promise was successful or not. This is similar to Jim giving you his promise that he will pay you back after work. When we create this promise object it takes a function with two parameters: resolve and reject. These parameters are functions that correlate with a success and failure state.

The resolve function is the success function and should be called whenever the promise was successful. This replaces our successCallback.

The reject function is the error function and should be called whenever the promise was not able to be completed successfully. This replaces our errorCallback.

The next big difference in these examples is how we call handleJimWork. In the callback version we just passed the callbacks to handleJimWork, but in the promise example we don't actually pass any callbacks to handleJimWork. Instead we use the promise returned from handleJimWork to check for success/failure. On the promise we call .then and .catch to check for success or failure.

If the promise in handleJimWork calls the resolve method then all of the code in .then is run. This is why we put the successful callback in the .then.

If the promise in handleJimWork calls the reject method then all of the code in .catch is run. This is why we put the error callback in the .catch.

The best way to think of promises is to just think of resolve as the same as .then and reject as the same as .catch. It also helps to think of promises in terms of plain English.

Jim promises to go to work and if this resolves successfully then he will pay us $100. If for some reason the promise is rejected by Jim not going to work, breaking his leg, or some other reason, then we need to catch that failure and be prepared to handle it accordingly.

purushothaman-source commented 1 year ago

One last thing to know about .then and .catch is that you can actually pass a parameter down to each.

function handleJimWork() {
  return new Promise((resolve, reject) => {
    // Slow method that runs in the background
    const success = doJimWork()
    if (success) {
      resolve(100)
    } else {
      reject("Jim broke his leg")
    }
  })
}

handleJimWork().then(amount => {
  console.log(`Jim paid you ${amount} dollars`)
}).catch(reason => {
  console.error(`Error: ${reason}`)
})

In the above example I modified our code slightly so that now the resolve and reject methods are actually called with a parameter. In the case of resolve we pass in the amount of money that Jim pays us back and in the reject case we pass in the reason why Jim cannot pay us back. Then in .then and .catch we use the parameters passed to resolve and reject to give us more detailed information about what happens. You may also notice I simplified our code a bit by not extracting the result of handleJimWork into its own promise variable. In most cases when you write promises you will just directly chain .then and .catch onto the end of the function instead of creating a variable to store the promise in.

purushothaman-source commented 1 year ago

Promise Chaining

Just by looking at the above examples promises may not seem that great, but the real power of promises comes in the ability to chain them together which solves the problem of callback hell.

function one(callback) {
  doSomething()
  callback()
}

function two(callback) {
  doSomethingElse()
  callback()
}

function three(callback) {
  doAnotherThing()
  callback()
}

one(() => {
  two(() => {
    three(() => {
      console.log("We did them all")
    })
  })
})

In the above example we have three functions that all do something and we need to call them in order so once the first function finishes we call the second and so on. Then finally at the end we log out that they have all three finished. This is a common problem in JavaScript and with callbacks you start to run into a nested mess as you can see. With promises, though, there is no nested mess to worry about since you can chain promises.

function one() {
  return new Promise(resolve => {
    doSomething()
    resolve()
  })
}

function two() {
  return new Promise(resolve => {
    doSomethingElse()
    resolve()
  })
}

function three() {
  return new Promise(resolve => {
    doAnotherThing()
    resolve()
  })
}

one().then(() => {
  return two()
}).then(() => {
  return three()
}).then(() => {
  console.log("We did them all")
})

In this example we are calling one and in the first .then we are calling two and returning the promise two returns. Since we are returning a promise from a .then JavaScript is smart enough to run the code in that promise and once it finishes call the next .then in the chain. This is repeated again with three and we finally get the log printed at the end. We can even clean this up a bit further.

one()
  .then(two)
  .then(three)
  .then(() => {
    console.log("We did them all")
  })

The above code works exactly the same since the functions two and three return promises when called.

purushothaman-source commented 1 year ago

Advanced Promise Features

The .finally method works very similar to .then and .catch in that it is chained onto a promise, but the code in .finally will run whether the promise fails or succeeds.

handleJimWork()
  .then(amount => {
    console.log(`Jim paid you ${amount} dollars`)
  }).catch(reason => {
    console.error(`Error: ${reason}`)
  }).finally(() => {
    console.log("This always runs")
  })

.finally is great if you need to do some clean up or you want to do the same thing whether a promise succeeds or fails.

purushothaman-source commented 1 year ago

Promise.all

The rest of the methods in this section will all be on the Promise object itself. The Promise.all method takes an array of promises and will wait for all of them to resolve before calling .then with the results of all the promises. If any of the promises reject, though, it will immediately call .catch with the error of the failed promise.

function one() {
  return new Promise(resolve => {
    doSomething()
    resolve("From One")
  })
}

function two() {
  return new Promise(resolve => {
    doSomethingElse()
    resolve("From Two")
  })
}

Promise.all([
  one(),
  two()
]).then(messages => {
  console.log(messages)
  // ["From One", "From Two"]
}).catch(error => {
  // First error if any error
})
purushothaman-source commented 1 year ago

Promise.allSettled

This method is very similar to Promise.al. The only difference is that Promise.allSettled will wait for all promises to succeed and/or fail before calling .then. Promise.allSettled also never calls .catch and instead will tell you if each promise failed or succeeded in the .then.

Promise.allSettled([
  one(),
  two()
]).then(messages => {
  console.log(messages)
  /* [
    { status: "fulfilled", value: "From One" },
    { status: "fulfilled", value: "From Two" }
  ] */
})
purushothaman-source commented 1 year ago

Promise.any

This method takes an array of promise just like the previous methods, but it will only wait for one promise to resolve. Once one promise in the list is successful it will call .then with the result of the first successful promise.

Promise.any([
  one(),
  two()
]).then(firstMessage => {
  console.log(firstMessage)
  // Message from whichever resolved first
}).catch(error => {
  // Generic error saying all promises failed
})
purushothaman-source commented 1 year ago

Promise.race

This method is very similar to Promise.any, but it only waits until one promise either fails or succeeds unlike Promise.any which only cares about the first success. Promise.race will wait until the first promise fails or succeeds and then call .then or .catch accordingly.

Promise.race([
  one(),
  two()
]).then(firstMessage => {
  console.log(firstMessage)
  // Message from first promise to finish if it was a success
}).catch(firstError => {
  // Message from first promise to finish if it was an error
})
purushothaman-source commented 1 year ago

Promise.resolve

This method is a shorthand for returning a promise that resolves immediately. This is useful if you need to pass a promise to something but do not already have a promise.

Promise.resolve(200).then(amount => {
  console.log(amount)
  // 200
})
purushothaman-source commented 1 year ago

Promise.reject

This method is the same as Promise.resolve, but for returning a failing promise.

Promise.reject("Error").catch(message => {
  console.error("Error")
  // Error
})
purushothaman-source commented 1 year ago

Conclusion

Promises are incredibly versatile and much easier to work with then callbacks. This is why any time I need to deal with async code I always reach for a promise over a callback.

purushothaman-source commented 1 year ago

Async/Await

Promises are one of the best additions to JavaScript because they make handling async code so much easier. Going from callbacks to promises feels like a massive upgrade, but there is something even better than promises and that is async/await. Async/await is an alternative syntax for promises that makes reading/writing async code even easier, but there are a few caveats you need to know about async/await or you may end up making your code worse.

purushothaman-source commented 1 year ago

Async/Await Basics

In order to understand async/await it is easiest to start with an example of promises being used and then convert that to async/await. To get started we are going to use the below function in all of our examples.

function setTimeoutPromise(delay) {
  return new Promise((resolve, reject) => {
    if (delay < 0) return reject("Delay must be greater than 0")

    setTimeout(() => {
      resolve(`You waited ${delay} milliseconds`)
    }, delay)
  })
}

This function is simply a promise based version of setTimeout. Now let's look at how we would chain together two timeouts where the second timeout waits for the first to finish.

setTimeoutPromise(250).then(msg => {
  console.log(msg)
  console.log("First Timeout")
  return setTimeoutPromise(500)
}).then(msg => {
  console.log(msg)
  console.log("Second Timeout")
})
// Output:
// You waited 250 milliseconds
// First Timeout
// You waited 500 milliseconds
// Second Timeout
purushothaman-source commented 1 year ago

If you are familiar with promises this code shouldn't be too confusing. The most confusing part of the code is that we are returning the second promise from the first so we can chain them together. Now this code works fine, but we can make it a lot cleaner with async/await.

doStuff()
async function doStuff() {
  const msg1 = await setTimeoutPromise(250)
  console.log(msg1)
  console.log("First Timeout")

  const msg2 = await setTimeoutPromise(500)
  console.log(msg2)
  console.log("Second Timeout")
}
// Output:
// You waited 250 milliseconds
// First Timeout
// You waited 500 milliseconds
// Second Timeout
purushothaman-source commented 1 year ago

The above code does the exact same thing as the previous version, but you will notice it looks much more like normal synchronous code which is the point of async/await. Now let's talk about how this code works.

First you will notice that we wrapped all our code in a function called doStuff. The name of this function is not important, but you will notice that we labeled this function as async by putting the async keyword before the function keyword. Doing this tells JavaScript that we plan to use the await keyword within this function. If we do not label the function as async and use the await keyword within the function it will throw an error

It is also important to note that as of now you cannot use the await keyword unless you are inside a function which is why we had to create a function to run this code. This is something that JavaScript is planning to change by adding top level await which means you can use await at the top level of a file without being in a function, but this is still not in any browsers.

Now that we understand the async keyword let's talk about the code in the function. You will notice it looks very similar to the previous code and that is because async/await is just a different way of writing the same thing. To convert a promise .then to async/await you need to take the promise function call setTimeoutPromise(250) and put the keyword await in front of it. This tells JavaScript that the function after the await keyword is asynchronous. Then if your promise returns a value you can just access that value as if it was a normal function return like we did with const msg1 = await setTimeoutPromise(250). The final step is to take all the code from the .then portion of the promise and put it after the function call.

The reason this works is because when JavaScript sees the eawait keyword it will call the awaited function, but it will not run any of the code after that function until the promise returned by that function is resolved. Instead JavaScript will run code other places in your application while it waits for the promise to resolve. Once the promise resolves it will return the promise result from the awaited function and run all the code up until the next await statement and repeat.

purushothaman-source commented 1 year ago

Catching Errors

The above section covers the absolute basics of async/await, but what happens if your promise rejects instead of resolving. This is easy to catch with traditional promise syntax.

setTimeoutPromise(-10).then(msg => {
  console.log(msg)
}).catch(error => {
  console.error(error)
})
// Output:
// Delay must be greater than 0

With async/await this is a bit more complicated.

doStuff()
async function doStuff() {
  try {
    const msg = await setTimeoutPromise(-10)
    console.log(msg)
  } catch (error) {
    console.error(error)
  }
  console.log("Outside")
}
// Output:
// Delay must be greater than 0
// Outside

In order to catch an error you need to wrap your code in a try/catch block. All the code that could possible fail (such as the promises) needs to be put in the try section of the try/catch block. JavaScript will try to run this code and at any point if there is an error it will stop running the code in the try block and jump to the catch block.

The catch block of the code takes in a single error parameter. If there was an error in the program this code will run and then the program will continue on after the try/catch block as normal. If there was no error the code will run through the entire try block, skip the catch block, and continue on as normal.

You can also handle the finally portion of promises in the same way.

setTimeoutPromise(-10).then(msg => {
  console.log(msg)
}).catch(error => {
  console.error(error)
}).finally(() => {
  console.log("Runs no matter what")
})
// Output:
// Delay must be greater than 0
// Runs no matter what
doStuff()
async function doStuff() {
  try {
    const msg = await setTimeoutPromise(-10)
    console.log(msg)
  } catch (error) {
    console.error(error)
  } finally {
    console.log("Runs no matter what")
  }
}
// Output:
// Delay must be greater than 0
// Runs no matter what
purushothaman-source commented 1 year ago

Async/Await Caveats

Async/await is incredible when dealing with asynchronous code, but if you need to deal with asynchronous code that runs in parallel it does not work.

Imagine a scenario where you are looping through a set of values and want to do something with these values that is asynchronous.

for (let i = 0; i < 10; i++) {
  getUser(i).then(user => console.log(user))
}

This code will create 10 promises that run in the background all at once and will log out all 10 users at approximately the same time if the getUser function takes the same time to run each time it is called.

await function doStuff() {
  for (let i = 0; i < 10; i++) {
    const user = await getUser(i)
    console.log(user)
  }
}

You may think the above code does the same thing, but this will actually run each getUser function one after the other. The reason for this is because in the first iteration of the loop we call getUser and wait for it to get the user before moving on. Once it gets the user we log it out and then wait to get the next user. This repeats for all users.

With the .then promise based version we are never waiting for the user to continue which means that we go through the entire loop calling getUser without doing any waiting. This means that if the getUser function takes 50ms to run the first example will call all getUser functions and then 50ms later it will print out all users. The second example will call the first getUser function, wait 50ms, print out the user, and then call getUser a second time before waiting 50ms and repeating. This means the async/await version will take 500ms to run through all the users.

Because of this I recommend never using async/await in a loop unless you specifically need to wait for each previous iteration of the loop before the next iteration can be completed.

purushothaman-source commented 1 year ago

Conclusion

While async/await doesn't allow you to do anything new in JavaScript it is still incredibly useful because of how much cleaner it can make the code you write.