Callbacks and Promises in Javascript

satya - 11/29/2024, 8:28:50 PM

About

  1. Async features in JS are bread and butter for React and other modern JS programming practices
  2. By their syntax they look foreign to a casual reader or one that is returning to JS after few years
  3. The syntax is terse and brief
  4. Understand "comprehensively" all the ways async is done in JS, and by extension hopefully in python and programming languages as well
  5. See how the following 3 ways work under the hood
  6. Call backs
  7. Promises
  8. async and await
  9. Explore and understand Promises lot better by stripping away the syntactical sugar
  10. See samples for each
  11. Provide explanation for each sample
  12. Get a sense of "reading" the abbreviated syntax
  13. Also understand the arrow functions which are heavily used in async programming

satya - 11/29/2024, 8:55:45 PM

Arrow and anonymous functions

  1. Like functions, they are blocks of code that can be executed
  2. One that executes them can call them with arguments (more like inputs)
  3. They don't have names, instead they are assigned to variables
  4. They don't crowd the name space of functions and hence do not conflict with named functions
  5. An anonymous function is a function that has "function" as a key word and not have a name, but instead just assigned to a variable.
  6. An arrow function also has no name, so is anonymous, but is defined with arrow syntax
  7. They differ in their "this" argument.
  8. The anonymous function with a function expression carries the this variable
  9. The arrow function carries the parent's this variable

satya - 11/29/2024, 8:59:58 PM

Code snippets to explain all these variations


// 1. Function Expressions

// Anonymous Function Expression
const add = function (a, b) {
  return a + b;
};
console.log(add(2, 3)); // 5

// Named Function Expression
const multiply = function multiplyFunc(a, b) {
  return a * b;
};
console.log(multiply(2, 3)); // 6

// Immediately Invoked Function Expression (IIFE)
(function () {
  console.log("IIFE called immediately!");
})(); // Output: IIFE called immediately!

// 2. Arrow Functions

// Single-Parameter Arrow Function (Implicit Return)
const square = x => x * x;
console.log(square(4)); // 16

// Multiple Parameters (Explicit Return)
const subtract = (a, b) => {
  return a - b;
};
console.log(subtract(5, 2)); // 3

// No Parameters
const greet = () => "Hello, World!";
console.log(greet()); // Hello, World!

// Arrow Function Returning an Object
const createUser = (name, age) => ({ name, age });
console.log(createUser("Alice", 25)); // { name: "Alice", age: 25 }

// Arrow Function with Rest Parameters
const sum = (...nums) => nums.reduce((total, num) => total + num, 0);
console.log(sum(1, 2, 3, 4)); // 10

// Arrow Function as a Callback
const numbers = [1, 2, 3];
const doubled = numbers.map(num => num * 2);
console.log(doubled); // [2, 4, 6]

// 3. Function Context and `this`

// Regular Function with `this`
const obj = {
  value: 10,
  regularFunction: function () {
    console.log(this.value); // `this` refers to obj
  },
};
obj.regularFunction(); // 10

// Arrow Function with `this`
const obj2 = {
  value: 10,
  arrowFunction: () => {
    console.log(this.value); // `this` refers to the outer context
  },
};
obj2.arrowFunction(); // undefined (or error in strict mode)

// 4. Advanced Arrow Function Syntax

// Chained Arrow Functions
const addChained = a => b => c => a + b + c;
console.log(addChained(1)(2)(3)); // 6

// Arrow Function in Object Methods (Caution with `this`)
const obj3 = {
  value: 42,
  method: () => {
    console.log(this.value); // `this` does not refer to obj3
  },
};
obj3.method(); // undefined

// Event Listener with Arrow Function
document.body.addEventListener("click", () => {
  console.log("Body clicked!");
});

// 5. Differences in `arguments`

// Regular Function Using `arguments`
function logArguments() {
  console.log(arguments); // [Arguments] { '0': 1, '1': 2, '2': 3 }
}
logArguments(1, 2, 3);

// Arrow Function and Rest Parameters
const logArgumentsArrow = (...args) => {
  console.log(args); // [1, 2, 3]
};
logArgumentsArrow(1, 2, 3);

// 6. Immediately Returning Arrow Function

// Simple Implicit Return
const triple = x => x * 3;
console.log(triple(3)); // 9

// Returning an Object
const getObject = () => ({ key: "value" });
console.log(getObject()); // { key: "value" }

satya - 11/29/2024, 9:07:59 PM

The prototypical arrow function


const square = x => x * x;
console.log(square(4)); // 16

satya - 11/29/2024, 9:09:37 PM

Explanation

  1. It is recognized by the arrow =>
  2. The left of it is the set of arguments
  3. The arguments will be inside (arg1,arg2) if there are more than one
  4. To the right of the arrow is the body of the function
  5. If it is more than one line, it will be in curly braces
  6. The right hand is returned as the value

satya - 11/29/2024, 9:10:50 PM

It will look like the following


const square = (x) => {
  return x * x;
};

satya - 11/29/2024, 9:12:44 PM

One with no arguments


// No Parameters
const greet = () => "Hello, World!";
console.log(greet()); // Hello, World!

satya - 11/29/2024, 9:14:58 PM

A more complicated example


// Arrow Function with Rest Parameters
const sum = (...nums) => nums.reduce((total, num) => total + num, 0);
console.log(sum(1, 2, 3, 4)); // 10

satya - 11/29/2024, 9:19:24 PM

Explanation

  1. The arrow function "sum" takes a variable number of args
  2. the are represented by the arg signature of ...nums
  3. In the body of the function "nums" is an array (by the convention of ...)
  4. The array's reduce function takes as its input another "user supplied" function and also an integer as the initial value for reduction as it goes through each element of the array to reduce it (or sum it, in this case)
  5. The user supplied function is un named
  6. So it is an arrow function
  7. The "reduce" demands that, the arrow function take 2 arguments
  8. The current total as total and the current num in the sequence

satya - 11/29/2024, 9:20:31 PM

If you read the lines above and get used to understanding their structure, you have it

  1. This whole exercise is to get oneself familiar with the syntax
  2. Some times this feature of passing around functions as if they are variables is also called higher order functions.

satya - 11/29/2024, 9:21:06 PM

What are higher order functions? Read the following if you need further explanation

What are higher order functions?

Search for: What are higher order functions?

satya - 11/29/2024, 9:22:36 PM

In summary, best way to read arrow functions

  1. Look for the arrow :)
  2. Left of it are the arguments to the function
  3. To the right is the body

satya - 11/29/2024, 9:28:47 PM

Javascript arrow function: See syntax of these as images

Show images for: Javascript arrow function

satya - 11/30/2024, 9:22:42 AM

Javascript Promise

Show images for: Javascript Promise

satya - 11/30/2024, 9:24:23 AM

The three ways of asyncing in JS

  1. Traditional (Async) Callbacks
  2. Promises
  3. async, await using Promises as the backbone

satya - 11/30/2024, 9:27:17 AM

Here is an async call back example that deals with both success and failiure


// Define a function that accepts success and error callbacks
function fetchData(callback, errorCallback) {
  const success = false; // Simulate an error scenario

  if (success) {
    callback("Data received!");
  } else {
    errorCallback("Error fetching data.");
  }
}

// Call the function with success and error callbacks
fetchData(
  (message) => {
    console.log(message); // Success callback
  },
  (error) => {
    console.error(error); // Error callback
  }
);

satya - 11/30/2024, 9:32:20 AM

Notes on traditional callbacks

  1. fetchdata although is acting as an async, it is defined just like a normal function, in that sense it is quite traditional way of call backs.
  2. It takes 2 functions as inputs to call them back. In an OO world these would have been objects or an object with 2 methods to call back :)
  3. When the method is invoked the arrow functions are used as callbacks
  4. The argument to the call backs is determined by the contract with the fetchdata function.
  5. The documentation of fetchdata should say this! hopefully in its header documentation
  6. Another pattern is to have the callback function receive the first argument the result code (good or bad) and the second argument and on the actual results

satya - 11/30/2024, 9:35:06 AM

Here is an older filesystem calls in node.js does this


const fs = require("fs");
fs.readFile("example.txt", "utf8", (err, data) => {
  if (err) {
    console.error("Error reading file:", err);
  } else {
    console.log("File content:", data);
  }
});

satya - 11/30/2024, 9:35:42 AM

Notice

  1. Use the callback of 2 args, the first "err" and the second "data"

satya - 11/30/2024, 9:36:50 AM

Here is how they are used in streaming.....nice example


const fs = require("fs");
const stream = fs.createReadStream("example.txt");
stream.on("data", chunk => {
  console.log("Chunk received:", chunk);
});
stream.on("end", () => {
  console.log("No more data.");
});

satya - 11/30/2024, 9:37:44 AM

Here is what happens there are multiple asyncs....


getData((err, data) => {
  if (err) return console.error(err);
  processData(data, (err, processed) => {
    if (err) return console.error(err);
    saveData(processed, (err) => {
      if (err) return console.error(err);
      console.log("All done!");
    });
  });
});

satya - 11/30/2024, 9:38:30 AM

Notice the intentions

  1. get data first
  2. process the data
  3. save the data in the end

satya - 11/30/2024, 9:39:26 AM

This is done better using promises or a language that is closer to the intention


getData()
  .then(processData)
  .then(saveData)
  .then(() => console.log("All done!"))
  .catch(err => console.error(err));

//Each method returns a promise for chaining the thens