Callback (Hell)

&

Promise (Land)

Our Setup 💻

JSON Server (Data)

Users

Posts

Comments

Live Server (Code)

index.html

callback.js

promise.js

Flow 1/3

Step Type
1. Select random user, by id sync
2. Fetch all of user's posts async
3. Select random post id sync

Flow 2/3

Step Type
4. Fetch all of post's comments async
5. Select random comment id sync
6. Fetch selected comment async

Flow 3/3

Step Type
7.  Update comment sync
8. Push updated comment (PUT) async
9. Fetch updated comment async
10. Log updated comment sync

5 Asynchronous Tasks

We're chaining

  🔥 Callback Hell 🔥

function getUserPostComment() {
    // ...
    fetchData(
        `${POSTS_URL}?userId=${userIndex}`,
        function success(posts) {
            // ...
            fetchData(
                `${COMMENTS_URL}?postId=${postId}`,
                function success(comments) {
                    // ...
                    fetchData(
                        `${COMMENTS_URL}/${commentId}`,
                        function success() {
                            // ...
                            fetchData(
                                `${COMMENTS_URL}/${commentId}`,
                                function success(response) {
                                    // ...
                                },
                                function error(err) {
                                    console.error(err);
                                }
                            )
                        },
                        function error(err) {
                            console.error(err);
                        },
                        "PUT",
                        JSON.stringify(updateComment)
                    )

                },
                function error(err) {
                    console.error(err);
                }
            )
        },
        function error(err) {
            console.error(err);
        }
    )
}

getUserPostComment()

Pyramid

Of

DOOM?


function getUserPostComment() {
  const userIndex = generateRandomPositiveNumber(0, 10);
  console.debug("Step 1: fetch user's posts");

  fetchData(
    `${POSTS_URL}?userId=${userIndex}`,
    fetchPostComment,
    error
  )
}

function fetchPostComment(posts) {
  console.log(`User has ${posts.length} posts.`);

  const count = posts.length
  const postIndex = generateRandomPositiveNumber(0, count - 1);
  const post = posts[postIndex]
  const { id: postId } = post

  console.debug("Step 2: fetch post's comment.");
  fetchData(
    `${COMMENTS_URL}?postId=${postId}`,
    modifyCommentBody,
    error
  )
}

function modifyCommentBody(comments) {
  const count = comments.length
  const commentIndex = generateRandomPositiveNumber(0, count - 1);
  const comment = comments[commentIndex]
  const { id: commentId } = comment

  console.log("Original comment:", comment);

  const updateComment = { ...comment, body: "Body was changed!" }

  console.debug("Step 3: modify the comment's body.");
  fetchData(
    `${COMMENTS_URL}/${commentId}`,
    fetchUpdatedComment.bind(null, commentId),
    error,
    "PUT",
    JSON.stringify(updateComment)
  )

}

function fetchUpdatedComment(commentId) {
  console.debug("Step 4: fetch the updated comment.");
  fetchData(
    `${COMMENTS_URL}/${commentId}`,
    logUpdatedComment,
    error
  )
}

function logUpdatedComment(comment) {
  console.log("Comment was updated:", comment);
}

function error(err) {
  const { statusText, responseURL } = err

  const parts = responseURL.split("/")
  const lastButOneIndex = parts.length - 2
  const entityPart = parts[lastButOneIndex]
  const entity = entityPart.substring(0, entityPart.length - 1)
  console.error("The", entity, "was", statusText.toLowerCase(), err);
}

Problem Solved!

<demo>


function getUserPostComment() {
  const userIndex = generateRandomPositiveNumber(0, 10);
  console.debug("Step 1: fetch user's posts");

  fetchData(
    `${POSTS_URL}?userId=${userIndex}`,
    fetchPostComment,
    error
  )
}

function fetchPostComment(posts) {
  console.log(`User has ${posts.length} posts.`);

  const count = posts.length
  const postIndex = generateRandomPositiveNumber(0, count - 1);
  const post = posts[postIndex]
  const { id: postId } = post

  console.debug("Step 2: fetch post's comment.");
  fetchData(
    `${COMMENTS_URL}?postId=${postId}`,
    modifyCommentBody,
    error
  )
}

function modifyCommentBody(comments) {
  const count = comments.length
  const commentIndex = generateRandomPositiveNumber(0, count - 1);
  const comment = comments[commentIndex]
  const { id: commentId } = comment

  console.log("Original comment:", comment);

  const updateComment = { ...comment, body: "Body was changed!" }

  console.debug("Step 3: modify the comment's body.");
  fetchData(
    `${COMMENTS_URL}/${commentId}`,
    fetchUpdatedComment.bind(null, commentId),
    error,
    "PUT",
    JSON.stringify(updateComment)
  )

}

function fetchUpdatedComment(commentId) {
  console.debug("Step 4: fetch the updated comment.");
  fetchData(
    `${COMMENTS_URL}/${commentId}`,
    logUpdatedComment,
    error
  )
}

function logUpdatedComment(comment) {
  console.log("Comment was updated:", comment);
}

function error(err) {
  const { statusText, responseURL } = err

  const parts = responseURL.split("/")
  const lastButOneIndex = parts.length - 2
  const entityPart = parts[lastButOneIndex]
  const entity = entityPart.substring(0, entityPart.length - 1)
  console.error("The", entity, "was", statusText.toLowerCase(), err);
}

Problem Solved!

Not So Fast!

Step 1 knows about step 2

which knows about step 3

which knows about step 4

function getUserPostComment() {
  const userIndex = generateRandomPositiveNumber(0, 10);
  console.debug("Step 1: fetch user's posts");

  fetchData(
    `${POSTS_URL}?userId=${userIndex}`,
    fetchPostComment,
    error
  )
}

function fetchPostComment(posts) {
  console.log(`User has ${posts.length} posts.`);

  const count = posts.length
  const postIndex = generateRandomPositiveNumber(0, count - 1);
  const post = posts[postIndex]
  const { id: postId } = post

  console.debug("Step 2: fetch post's comment.");
  fetchData(
    `${COMMENTS_URL}?postId=${postId}`,
    modifyCommentBody,
    error
  )
}

function modifyCommentBody(comments) {
  const count = comments.length
  const commentIndex = generateRandomPositiveNumber(0, count - 1);
  const comment = comments[commentIndex]
  const { id: commentId } = comment

  console.log("Original comment:", comment);

  const updateComment = { ...comment, body: "Body was changed!" }

  console.debug("Step 3: modify the comment's body.");
  fetchData(
    `${COMMENTS_URL}/${commentId}`,
    fetchUpdatedComment.bind(null, commentId),
    error,
    "PUT",
    JSON.stringify(updateComment)
  )

}

function fetchUpdatedComment(commentId) {
  console.debug("Step 4: fetch the updated comment.");
  fetchData(
    `${COMMENTS_URL}/${commentId}`,
    logUpdatedComment,
    error
  )
}

function logUpdatedComment(comment) {
  console.log("Comment was updated:", comment);
}

function error(err) {
  const { statusText, responseURL } = err

  const parts = responseURL.split("/")
  const lastButOneIndex = parts.length - 2
  const entityPart = parts[lastButOneIndex]
  const entity = entityPart.substring(0, entityPart.length - 1)
  console.error("The", entity, "was", statusText.toLowerCase(), err);
}

getUserPostComment()

Step 1 know about step 2

which know about step 3

which knows about step 4

I have to read the whole code to understand the flow of events.

I have no solutions to this yet. Maybe with functional programming? But that's not in the language.

Another Problem... 

There's no `return` from 

🔥 Callback Hell! 🔥

`return` does not work

neither does`throw`

So for error handling ...

well good luck 💁🏼‍♂️

<demo>

Welcome to

Promise Land  🌴

⛓ Promise  ⛓ 

⛓ Chaining ⛓ 

Step 1 does NOT

know about step 2

AND WE DON'T CARE! ✌🏼


function getUserPostComment() {
  const userIndex = generateRandomPositiveNumber(0, 10);
  console.debug("Step 1: fetch user's posts");

  return fetchData(`${POSTS_URL}?userId=${userIndex}`)
}

function fetchPostComment(posts) {
  console.log(`User has ${posts.length} posts.`);

  const count = posts.length
  const postIndex = generateRandomPositiveNumber(0, count - 1);
  const post = posts[postIndex]
  const { id: postId } = post || {}

  console.debug("Step 2: fetch post's comment.");
  return fetchData(`${COMMENTS_URL}?postId=${postId}`)
}

function modifyCommentBody(comments) {
  const count = comments.length
  const commentIndex = generateRandomPositiveNumber(0, count - 1);
  const comment = comments[commentIndex]
  const { id: commentId } = comment || {}

  console.log("Original comment:", comment);

  const updateComment = { ...comment, body: "Body was changed!" }

  console.debug("Step 3: modify the comment's body.");
  return fetchData(
    `${COMMENTS_URL}/${commentId}`,
    "PUT",
    JSON.stringify(updateComment)
  )
}

function fetchUpdatedComment({ id: commentId }) {
  console.debug("Step 4: fetch the updated comment.");
  return fetchData(`${COMMENTS_URL}/${commentId}`)
}

function logUpdatedComment(updatedComment) {
  console.log("Comment was updated:", updatedComment);
}

function error(err) {
  const { statusText, responseURL } = err

  const parts = responseURL.split("/")
  const lastButOneIndex = parts.length - 2
  const entityPart = parts[lastButOneIndex]
  const entity = entityPart.substring(0, entityPart.length - 1)
  console.error("The", entity, "was", statusText.toLowerCase(), err);
}

getUserPostComment()
  .then(fetchPostComment)
  .then(modifyCommentBody)
  .then(fetchUpdatedComment)
  .then(logUpdatedComment)
  .catch(error)

Which

leads us

to this...

function getUserPostComment() { /* ... */ }

function fetchPostComment(posts) { /* ... */ }

function modifyCommentBody(comments) { /* ... */ }

function fetchUpdatedComment({ id: commentId }) { /* ... */ }

function logUpdatedComment(updatedComment) { /* ... */ }

function error(err) { /* ... */ }

getUserPostComment()
  .then(fetchPostComment)
  .then(modifyCommentBody)
  .then(fetchUpdatedComment)
  .then(logUpdatedComment)
  .catch(error)

  I DON'T CARE ABOUT

  IMPLEMENTATION

  DETAILS ANYMORE

<demo>

Beware ...

Promises can be

poorly used ...


function getUserPostComment() {
	// ...
    fetchData(`${POSTS_URL}?userId=${userIndex}`)
        .then(function fetchPostComment(posts) {
            // ...
            fetchData(`${COMMENTS_URL}?postId=${postId}`)
                .then(function modifyCommentBody(comments) {
                    // ...
                    fetchData(
                        `${COMMENTS_URL}/${commentId}`,
                        "PUT",
                        JSON.stringify(updateComment)
                    )
                        .then(function fetchUpdatedComment(commentId) {
                            // ...
                            fetchData(
                                `${COMMENTS_URL}/${commentId}`)
                                .then(function logUpdatedComment(comment) {
                                    console.log("Comment was updated:", comment);
                                })
                                .catch(error)
                        })
                        .catch(error)
                })
                .catch(error)
        })
        .catch(error)
}

function error(err) {
    const { statusText, responseURL } = err

    const parts = responseURL.split("/")
    const lastButOneIndex = parts.length - 2
    const entityPart = parts[lastButOneIndex]
    const entity = entityPart.substring(0, entityPart.length - 1)
    console.error("The", entity, "was", statusText.toLowerCase(), err);
}

getUserPostComment()

Recap

For chaining async work:

- Promises help us decouple our code

- Promises help us with error management

Thank You!

Callback Hell & Promises

By Cedric Poilly

Callback Hell & Promises

  • 216