Saturday, November 2, 2024

JavaScript Promise object and fetch, then and catch functions in JavaScript

Objectives:

  1. How to define a function so that it returns a promise?
  2. The resolve and reject functions
  3. How to know if a function returns a promise?
  4. fetch API method
  5. Promise object and its states
  6. then and catch methods
  7. Chaining promise methods

How to Define a Function that Returns a Promise

You can define a function to return a promise in two main ways: using a traditional promise with new Promise() or by defining the function as async.

Method 1: Using new Promise()

To create a promise manually, use the new Promise() constructor. Inside the constructor, define a callback function with two parameters, resolve and reject, which represent the success and failure outcomes of the promise, respectively.

Example:
function getData() {
  return new Promise((resolve, reject) => {
    const data = fetchData();  // Assume fetchData is a synchronous or async function

    if (data) {
      resolve(data);  // Promise is fulfilled successfully
    } else {
      reject('Data not found');  // Promise is rejected with an error
    }
  });
}

getData()
  .then(data => console.log('Data:', data))
  .catch(error => console.error('Error:', error));

Method 2: Using async Functions

The async keyword simplifies promise-based code by implicitly returning a promise. Any function declared as async will automatically return a promise, and within the function, you can use await to pause execution until another promise resolves.

Example:
async function getData() {
  try {
    const response = await fetch('https://api.example.com/data');
    if (!response.ok) throw new Error('Network response was not ok');
    const data = await response.json();
    return data;  // This will resolve the promise with data
  } catch (error) {
    throw error;  // This will reject the promise with the error
  }
}

getData()
  .then(data => console.log('Data:', data))
  .catch(error => console.error('Error:', error));

Key Differences

  1. Manual Promise Creation (new Promise): Gives you control over the resolve and reject logic and is ideal if you need more complex or conditional async handling.
  2. async Function: Automatically returns a promise and is simpler to write and read, especially when chaining multiple asynchronous operations.

Summary

  • To check if a function returns a promise: Use instanceof or refer to documentation.
  • To define a function that returns a promise: Use either the new Promise() approach or declare the function with the async keyword.

resolve and reject functions:

To create a promise manually, use the new Promise() constructor. Inside the constructor, define a callback function with two parameters, resolve and reject. The resolve and reject parameters in the new Promise() constructor are themselves callback functions.

Here’s how they work:

resolve: This function is called when the asynchronous operation completes successfully. Calling resolve(value) fulfills the promise with the specified value, which then becomes the resolved value of the promise. Any .then() attached to the promise will receive this value.

reject: This function is called if the asynchronous operation fails or if you want to handle an error condition. Calling reject(reason) marks the promise as rejected with the provided reason, typically an error message or error object. Any .catch() attached to the promise will handle this rejected state.

Example of resolve and reject as Callback Functions

In this example, resolve and reject are used as callbacks to indicate success or failure.
function checkNumber(num) {
  return new Promise((resolve, reject) => {
    if (num > 10) {
      resolve('Number is greater than 10');  // success case
    } else {
      reject('Number is 10 or less');       // failure case
    }
  });
}

// Using the promise
checkNumber(15)
  .then(message => console.log(message))   // Output: "Number is greater than 10"
  .catch(error => console.error(error));

checkNumber(5)
  .then(message => console.log(message))
  .catch(error => console.error(error));   // Output: "Number is 10 or less"

Why resolve and reject are Callback Functions
The resolve and reject functions are callbacks because they allow asynchronous code to "call back" into the promise to indicate the result of the operation. When you call either function, you’re "calling back" with either a success or failure result, and this triggers the corresponding .then() or .catch() handler on the promise.

So, resolve and reject are callback functions provided by the Promise constructor to control the outcome of the promise, depending on whether the operation succeeds or fails.

How to Know if a Function Returns a Promise

There are a few ways to check if a function returns a promise:

1. Check the Return Type:

You can check the return value of a function at runtime using instanceof. If the function returns a promise, it should be an instance of the Promise class:
javascript
function exampleFunction() {
  return new Promise((resolve, reject) => {
    resolve('Success!');
  });
}

const result = exampleFunction();
console.log(result instanceof Promise);  // true if it's a promise

2. Inspect the Function’s Documentation: Many JavaScript APIs (like fetch or async functions) are documented to indicate that they return promises. Checking the documentation is often the quickest way to confirm if a function returns a promise.

3. Look for async Functions: If a function is declared with the async keyword, it automatically returns a promise. So if you see async function, you know it’s returning a promise.

fetch API

The fetch function returns a promise object, which may be in pending, fulfilled, or rejected state. Initially, the promise is pending. When the promise is fulfilled, the callback function passed to then() is executed; if rejected, the callback function passed to catch() is executed. Each then() and catch() returns a new promise, allowing for chaining. Although it's usually cleaner to chain promises sequentially, then() and catch() can contain additional nested callbacks if needed for more complex logic.

Promise States: A promise has three possible states: pending, fulfilled, and rejected. 
  1. Initially, the promise returned by fetch() is indeed in the pending state until the request completes.
  2. When the promise is fulfilled (i.e., the network request is successful), the then() callback function is executed.
  3. If the promise is rejected (e.g., due to network issues), the catch() callback function is executed.
Error Handling with catch():
It's worth noting that catch() will handle any errors that occur in the promise chain before it, not just errors in the original fetch(). So if an error happens in any of the previous then() callbacks, it will also be caught by the catch().

Nesting promises:
In promises, rather than "nesting" then() and catch() calls directly inside each other, chaining them sequentially is more typical and cleaner. Each then() or catch() returns a new promise, allowing the chain to continue without literal nesting. However, if you want to handle different scenarios (like fallbacks), you can nest additional asynchronous calls within then() or catch() blocks. Just be cautious, as it can lead to more complex code.

Here’s how it works:

Chaining then() Calls: Since each then() also returns a new promise, you can chain multiple then() calls, where each callback function can contain more asynchronous logic.

Nested Callbacks: Inside a then() or catch() callback, you can nest other callbacks, either synchronous or asynchronous, to achieve more complex operations.

Here’s an example of nested then() and catch() callbacks:
fetch('https://api.example.com/data')
  .then(response => {
    if (!response.ok) throw new Error('Network response was not ok');
    return response.json();
  })
  .then(data => {
    console.log('Data received:', data);
    // Another asynchronous operation
    return fetch('https://api.example.com/another-endpoint');
  })
  .then(secondResponse => secondResponse.json())
  .then(secondData => {
    console.log('Second data received:', secondData);
  })
  .catch(error => {
    console.error('Fetch error:', error);
    // Handle the error, or try another nested operation
    return fetch('https://api.example.com/fallback-endpoint')
      .then(fallbackResponse => fallbackResponse.json())
      .then(fallbackData => console.log('Fallback data:', fallbackData))
      .catch(fallbackError => console.error('Fallback error:', fallbackError));
  });

Key Points
  • Each then() or catch() callback returns a new promise, allowing you to handle each step sequentially.
  • Nested callbacks allow handling different scenarios (like fallbacks on errors) within specific parts of the chain.
  • This can quickly become complex, so for readability, consider using async/await syntax as it can simplify nested promise logic by making the code look more synchronous.

Notes:
The then() and catch() are not callback functions themselves—they are methods provided by the Promise object. These methods take callback functions as arguments, which are then executed when the promise is fulfilled or rejected.

then() and catch() as Methods:

  • then() and catch() are methods that allow you to attach handlers (callbacks) to a promise.
  • When you call promise.then(...) or promise.catch(...), you're calling a method that accepts a function (callback) as an argument.

Callbacks Passed to then() and catch():

The actual functions you pass into then() and catch() are the callback functions. For example:
fetch('https://api.example.com/data')
  .then(response => console.log(response))  // This `response => console.log(response)` is a callback function.
  .catch(error => console.error(error));    // This `error => console.error(error)` is also a callback function.

Behavior of then() and catch():
  • The then() method is designed to handle the resolved value of a promise and automatically returns a new promise.
  • Similarly, catch() handles any error or rejection in the promise chain and also returns a new promise, allowing further chaining.
Summary
  • then() and catch() are methods on a promise, not callbacks.
  • The functions passed to then() and catch() are callbacks that are executed based on the promise’s outcome (fulfilled or rejected).

Chaining Promise methods

The fetch, then and catch functions return a promise so they can be chained. The fetch(), then(), and catch() functions each return a promise, which is what makes chaining possible in JavaScript. Here’s a more detailed look at how this works:

fetch() Returns a Promise:
  • The fetch() function initiates a network request and returns a promise that resolves to the Response object if the request is successful, or rejects if there’s a network error.
  • This initial promise can be chained by calling then() to handle the response, making it easy to perform further actions on the response data.
then() Returns a New Promise:
  • When you call then() on a promise, it processes the result of the previous promise and returns a new promise.
  • This new promise represents the completion of the callback function passed to then(), allowing you to chain additional operations.
  • This means you can keep calling then() on each returned promise, forming a promise chain.
catch() Also Returns a New Promise:
  • Similarly, the catch() method handles errors that occur in the promise chain and also returns a new promise.
  • This allows you to continue chaining even after handling an error.
Here’s an example to illustrate:
fetch('https://api.example.com/data')
  .then(response => {
    if (!response.ok) throw new Error('Network response was not ok');
    return response.json();
  })
  .then(data => {
    console.log('Data received:', data);
    return fetch('https://api.example.com/another-endpoint'); // This returns another promise
  })
  .then(secondResponse => secondResponse.json())
  .then(secondData => {
    console.log('Second data received:', secondData);
  })
  .catch(error => {
    console.error('Fetch error:', error);
  });

Summary
  • fetch(), then(), and catch() return promises, which makes chaining them possible.
  • This chaining allows for handling multiple asynchronous steps in sequence, with then() processing successful results and catch() handling any errors that arise.

No comments:

Post a Comment

Hot Topics