skip to content
luminary.blog
by Oz Akan
cup of java in binary

Best Async Document for Typescript

Asynchronous programming in TypeScript, callbacks, promises, await/async, RxJS.

/ 25 min read

Updated:
Table of Contents

JavaScript Engine & Runtime Environment

Javascript code runs in a JavaScript engine provided by the runtime environment.

Runtime EnvironmentJavaScript Engine
Node.jsV8
DenoV8
BunJavaScriptCore
FirefoxSpiderMonkey
ChromeV8
EdgeV8
SafariJavaScriptCore
IEChakraCore
OracleNashorn(deprecated)

In other words, runtime environment (Chrome, Node.js), provides the JavaScript engine (V8). The JavaScript engine provides a single threaded event loop (*) (**).

The JavaScript engine recognizes the functions of the Web/System APIs and hands them off to be handled by the runtime environment. Once those system functions (macro tasks like network, disk, DNS) are finished by the runtime environment, they return and are pushed onto the stack as callbacks. This is managed in the the event loop.

With JavaScript everything is a type of task.

  • console.log() statement is a task
  • const response = await axios.get(yourUrl) is a task
  • setTimeout(function, 100) is a task
  • const file = readFileSync('./filename.txt', 'utf-8'); is a task

Event loop is made of several queues. Tasks, like above, are grouped into different categories and are put into one of the event / task queues.

A special queue is the PromiseJobs queue where the promises created by the developer are processed. This queue is also called “microtask queue”. Practically everything else falls into “macrotasks queue”. Immediately after every macrotask, the engine executes all tasks from microtask queue, prior to running any other macrotasks or rendering (on the browser) or anything else.(*)(**).

Node.js is a runtime environment, doesn’t only run JavaScript but utilize multiple threads(for disk I/O etc). It’s the Node.js event loop which is single threaded. Whenever you do something like IO which is event based, it creates a new thread or utilizes an existing thread from the thread pool for it.

Chrome and Node.js share the same V8 JavaScript engine to run JavaScript but have different Event Loop implementations. The Chrome browser uses libevent as its event loop implementation, and Node.js uses libuv.

Let’s keep these statements in mind.

  • Blocking methods execute synchronously and non-blocking methods execute asynchronously.
  • JavaScript runs in a single thread, but the engine that runs JavaScript is not single threaded.

A Function

Let’s start with a simple function that prints the value provided.

function log(value: string) {
console.log(value);
}
log("hello world");

Output

Terminal window
hello world

This is a blocking function and executes synchronously. When it runs nothing else will be executed until it finishes. If it would take 10 minutes to run this function, everything else after this code block would have to wait for 10 minutes.

Let’s print a number of messages.

function log(value: string) {
console.log(value);
}
log("hello world 1");
log("hello world 2");
log("hello world 3");
log("hello world 4");
log("hello world 5");

Output

Terminal window
hello world 1
hello world 2
hello world 3
hello world 4
hello world 5

Pretty logical. Each log() function executed sequentially one after another.

Arrow Functions

Before we move forward let’s convert log() function into an arrow function.

const log = (value: string) => {
console.log(value);
};
log("hello world");

Output

Terminal window
hello world

Same behavior, we just used the arrow notation.

log() is a synchronous (arrow) function.

A Synchronous Function Which Waits

Let’s create a function that takes a while to run.

We have two different cases here:

  • A function that is cpu bound: a function that calculates a big number
  • A function that is waiting on external resources: disk IO, network IO etc.

CPU Bound Function

A CPU bound function, only needs CPU cycles to complete. The freeze() function below, waits for ms milliseconds and blocks the main loop. After ms milliseconds it finishes the execution.

const freeze = (ms: number) => {
const stopTime = Date.now() + ms;
while (Date.now() < stopTime) {} // Block the main loop
};

Let’s use freeze() function.

const freeze = (ms: number) => {
const stopTime = Date.now() + ms;
while (Date.now() < stopTime) {}
};
console.log("Start");
const start = Date.now();
freeze(2000);
const finish = Date.now();
console.log(`Ran for ${finish - start} milliseconds`);
console.log("Finish");

Output

Terminal window
Start
Ran for 2000 milliseconds
Finish

Everything above is as expected. JavaScript engine printed Start, then the time it took to run, then Finish.

If we had two tasks each taking 2 seconds, overall the program would run for 4 seconds.

const freeze = (ms: number) => {
const stopTime = Date.now() + ms;
while (Date.now() < stopTime) {}
};
console.log("Start");
const start = Date.now();
freeze(2000); // first task running for 2 seconds
freeze(2000); // second task running for 2 seconds
const finish = Date.now();
console.log(`Ran for ${finish - start} milliseconds`);
console.log("Finish");

Output

Terminal window
Start
Ran for 4000 milliseconds
Finish

setTimeout() Method

Executes a specified block of code once after a specified time has elapsed. The timer is provided by the runtime environment, not by the JavaScript Engine. (Example: Node.js implements setTimeout() method, not V8.)

setTimeout(function[, delay, arg1, arg2, …])

function A function to be executed after the timer expires.

delay (Optional) The time, in milliseconds (thousandths of a second), the timer should wait before the specified function or code is executed. If this parameter is omitted, a value of 0 is used, meaning execute “immediately”, or more accurately, the next event cycle. Note that in either case, the actual delay may be longer than intended; see Reasons for delays longer than specified below.

arg1, …, argN (Optional) Additional arguments which are passed through to the function specified by function.

return value The returned timeoutID is a positive integer value which identifies the timer created by the call to setTimeout(); this value can be passed to clearTimeout() to cancel the timeout.

  • NOTE: The specified amount of time (or the delay) is not the guaranteed time to execution, but rather the minimum time to execution. The callbacks you pass to these functions cannot run until the stack on the main thread is empty.

  • As a consequence, code like setTimeout(fn, 0) will execute as soon as the stack is empty, not immediately. If you execute code like setTimeout(fn, 0) but then immediately after run a loop that takes 10 seconds to complete, your callback will be executed after 10 seconds.

We already passed the threshold of lecture to code ratio. Let’s write some code to understand.

An anonymous function that writes hello world after 200 milliseconds.

const log = () => {
console.log("Hello World");
};
console.log("Start");
const start = Date.now();
setTimeout(log, 1000);
const finish = Date.now();
console.log(`Ran for ${finish - start} milliseconds`);
console.log("Finish");

Output

Terminal window
Start
Ran for 0 milliseconds
Finish
Hello World

This one is a little strange. Isn’t it? It seems to be out of order.

First we created a function named log() which prints hello world. Later we called setTimeout(log, 1000). This will wait for 1000 milliseconds and then call log() function. Remember, setTimeout() is actually a system call, that delegates the wait operation to Node.js. As soon as we call setTimeout(log, 1000), we push log() to the macrotasks queue we mentioned earlier. For this reason, code ran sequentially, as if it ignored setTimeout(log, 1000);. After 1000 milliseconds, log() function was pushed to the queue as a callback and executed. Finally, it printed Hello World.

Welcome to the async world of JavaScript.

Any function that sends a code block to a queue is an async function. Event loop is the code that checks every queue one after another and runs one or more tasks (code blocks) in them. TODO: improve wording here

An ugly representation is below; (TODO: Improve the presentation)

|
console.log("Start");
|
const start = Date.now();
|
--------------> push setTimeout(log, 1000); to macrotasks
|
const finish = Date.now();
|
console.log(`Ran for ${finish - start} milliseconds`);
|
console.log("Finish");
|
1000 milliseconds later
|
log();

By the way even if we set timeout value to 0 milliseconds, the code above would follow the exact same order. Let’s see.

const log = () => {
console.log("Hello World");
};
console.log("Start");
const start = Date.now();
setTimeout(log, 0); // same as setTimeout(log);
const finish = Date.now();
console.log(`Ran for ${finish - start} milliseconds`);
console.log("Finish");

Output

Terminal window
Start
Ran for 0 milliseconds
Finish
Hello World

Because setTimeout() is delegated to macrotasks queue, and because only one item from the macrotasks queue is pulled with every iteration of the loop, Hello World is printed at the very end.

The Problem

What if we needed to do something after the async function completes?

Let’s say we have two functions. getMessage() gets a message from the internet and printMessage() prints the message retrieved by getMessage(). It takes time for getMessage() to get the message.

getMessage();
printMessage();
// async function
const getMessage = () => {
console.log("getting the message");
setTimeout(() => {
console.log("retrieved the message");
}, 1000);
};
// not an async function
const printMessage = () => {
console.log("printing message");
};
getMessage();
printMessage();

Output

Terminal window
getting the message
printing the message
retrieved the message // after 1 second later

Not good. We printed the message before we received it.

I have an idea, let’s move printMessage() into getMessage()

const getMessage = () => {
console.log("getting the message");
setTimeout(() => {
console.log("retrieved the message");
}, 1000);
printMessage();
};
const printMessage = () => {
console.log("printing message");
};
getMessage();

Output

Terminal window
getting the message
printing the message
retrieved the message

Yikes. Let’s try again.

const getMessage = () => {
console.log("getting the message");
setTimeout(() => {
console.log("retrieved the message");
printMessage();
}, 1000);
};
const printMessage = () => {
console.log("printing message");
};
getMessage();

Output

Terminal window
getting the message
retrieved the message
printing the message

Yay. It worked. It worked but what if we want to do something different with the retrieved text? Do we need to change the getMessage() each time?

Callback Functions

Instead of changing the getMessage() each time, we can pass a function to printMessage().

const getMessage = (callback: Function) => {
console.log("getting the message");
setTimeout(() => {
console.log("retrieved the message");
// pretending like it take time to finish
callback();
}, 1000);
};
const printMessage = () => {
console.log("printing message");
};
getMessage(printMessage);

Output

Terminal window
getting the message
retrieved the message
printing the message

We are getting there. const getMessage = (callback: Function) => { here we are passing a callback function to getMessage() and calling back that function when we have what we were waiting for. We call the function a callback function because we call it back after we are done in the current function.

A better implementation is below. getMessage(index: number, cb: Function) accepts an index to know which message to get, and a function to call back after the message with the index number is received.

const getMessage = (index: number, cb: Function) => {
const messages = ["Hello World", "Hello Universe"];
const message = messages[index % messages.length];
console.log("LOG: getting the message");
setTimeout(() => {
console.log(`LOG: retrieved: "${message}"`);
cb(message);
}, 1000);
};
const printMessage = (message: string) => {
console.log("LOG: printing the message");
console.log(message);
};
getMessage(0, printMessage);

Output

Terminal window
LOG: getting the message
LOG: retrieved: "Hello World"
LOG: printing the message
Hello World

What if we wanted to add a transformation to the text we receive? Let’s convert the text to upper case before we print.

const getMessage = (index: number, cb: Function) => {
const messages = ["Hello World", "Hello Universe"];
const message = messages[index % messages.length];
console.log("LOG: getting the message");
setTimeout(() => {
console.log(`LOG: retrieved: "${message}"`);
cb(message);
}, 1000);
};
const upperCase = (message: string, cb: Function) => {
console.log("LOG: converting to upper case");
const upperCaseText = message.toUpperCase();
cb(upperCaseText);
};
const printMessage = (message: string) => {
console.log("LOG: printing the message");
console.log(message);
};
getMessage(0, (m: string) => upperCase(m, printMessage));

Not so easy to read, especially the very last line. We are calling getMessage() and passing upperCase() function as the callback. Then we are passing printMessage() as a callback to upperCase() function.

What if we wanted to receive messages in order we called them. Let’s modify the code a little bit so it will wait longer for messages[0] than it waits for messages[1].

const getMessage = (index: number, cb: Function) => {
const messages = ["Hello World", "Hello Universe"];
const durations = [1000, 500]; // index 0 has 1000 milliseconds
const message = messages[index % messages.length];
const duration = durations[index % messages.length];
console.log(`LOG: getting the message[${index}]`);
setTimeout(() => {
console.log(`LOG: retrieved: "${message}"`);
cb(message);
}, duration);
};
const upperCase = (message: string, cb: Function) => {
console.log("LOG: converting to upper case");
const upperCaseText = message.toUpperCase();
cb(upperCaseText);
};
const printMessage = (message: string) => {
console.log("LOG: printing the message");
console.log(message);
};
getMessage(0, (m: string) => upperCase(m, printMessage));
getMessage(1, (m: string) => upperCase(m, printMessage));

Output

Terminal window
LOG: getting the message[0]
LOG: getting the message[1]
LOG: retrieved: "Hello Universe"
LOG: converting to upper case
LOG: printing the message
HELLO UNIVERSE
LOG: retrieved: "Hello World"
LOG: converting to upper case
LOG: printing the message
HELLO WORLD

We tried to get messages[0], which is Hello World first but instead we got Hello Universe as the first message. We have to change the way we called the getMessage() function if we want to be able to get the messages in the order we called them.

getMessage(0, (m: string) => {
upperCase(m, printMessage);
getMessage(1, (m: string) => upperCase(m, printMessage));
});

Output

Terminal window
LOG: getting the message[0]
LOG: retrieved: "Hello World"
LOG: converting to upper case
LOG: printing the message
HELLO WORLD
LOG: getting the message[1]
LOG: retrieved: "Hello Universe"
LOG: converting to upper case
LOG: printing the message
HELLO UNIVERSE

What if we had one more message to print? Let’s add another message to messages[] and print all in order.

const getMessage = (index: number, cb: Function) => {
const messages = ["Hello World", "Hello Universe", "Hello Multiverse"];
const durations = [1000, 500, 750];
const message = messages[index % messages.length];
const duration = durations[index % messages.length];
console.log(`LOG: getting the message[${index}]`);
setTimeout(() => {
console.log(`LOG: retrieved: "${message}"`);
cb(message);
}, duration);
};
const upperCase = (message: string, cb: Function) => {
console.log("LOG: converting to upper case");
const upperCaseText = message.toUpperCase();
cb(upperCaseText);
};
const printMessage = (message: string) => {
console.log("LOG: printing the message");
console.log(message);
};
const convertAndPrint = (m: string) => {
upperCase(m, printMessage);
};
getMessage(0, (m: string) => {
upperCase(m, printMessage);
getMessage(1, (m: string) => {
upperCase(m, printMessage);
getMessage(2, (m: string) => upperCase(m, printMessage));
});
});

Output

Terminal window
LOG: getting the message[0]
LOG: retrieved: "Hello World"
LOG: converting to upper case
LOG: printing the message
HELLO WORLD
LOG: getting the message[1]
LOG: retrieved: "Hello Universe"
LOG: converting to upper case
LOG: printing the message
HELLO UNIVERSE
LOG: getting the message[2]
LOG: retrieved: "Hello Multiverse"
LOG: converting to upper case
LOG: printing the message
HELLO MULTIVERSE

Imagine we have a chain of these async function;

asyncFunction(function(){
asyncFunction(function(){
asyncFunction(function(){
asyncFunction(function(){
asyncFunction(function(){
....
});
});
});
});
});

This takes us to what is know as callback hell. Imagine adding error handling to the code above. It gets really hard to maintain. There are libraries to manage the chain of callbacks but promises emerged as a better option.

Promises

Promises in TypeScript provide a clean and robust way to handle asynchronous operations. They represent a value that may not be available yet, but will be resolved (fulfilled) at some point in the future, or may be rejected if an error occurs. Promises offer an improvement over traditional callbacks by allowing you to chain asynchronous operations in a more readable and manageable way, avoiding the infamous “callback hell.” (I still remember these endless callbacks…)

Syntax

// Function that returns a Promise
function fetchData(): Promise<string> {
return new Promise((resolve, reject) => {
// Simulate an asynchronous operation (e.g., network request)
setTimeout(() => {
const success = true; // Change to false to simulate an error
if (success) {
resolve("Data retrieved successfully!");
} else {
reject("Failed to retrieve data.");
}
}, 1000);
});
}
// Using the Promise with .then() and .catch()
fetchData()
.then((data) => {
console.log("Success:", data);
})
.catch((error) => {
console.error("Error:", error);
});

fetchData returns a Promise that simulates an asynchronous operation. .then() is used to handle the successful resolution of the Promise. .catch() is used to handle any errors that occur during the asynchronous operation.

Example

const upperCase = (message: string): string => {
console.log("LOG: converting to upper case");
const upperCaseText = message.toUpperCase();
return upperCaseText;
};
const printMessage = (message: string) => {
console.log("LOG: printing the message");
console.log(message);
};
let getMessage = (index: number): Promise<string> => {
return new Promise((resolve, reject) => {
const messages = ["Hello World", "Hello Universe", "Hello Multiverse"];
const durations = [1000, 500, 750];
const message = messages[index % messages.length];
const duration = durations[index % messages.length];
console.log(`LOG: getting the message[${index}]`);
setTimeout(() => {
console.log(`LOG: retrieved: "${message}"`);
resolve(message);
}, duration);
});
};
getMessage(1)
.then((message) => upperCase(message))
.then((message) => printMessage(message));
// we could shorten getMessage call as below
// getMessage(1).then(upperCase).then(printMessage);

Let’s break down the code piece by piece:

1. Uppercase Function

const upperCase = (message: string): string => {
console.log("LOG: converting to upper case");
const upperCaseText = message.toUpperCase();
return upperCaseText;
};
  • Purpose: This function receives a string (message), logs a message indicating that it’s converting the text, converts the string to uppercase using the toUpperCase() method, and returns the uppercase version.
  • Key Points:
    • Type Annotations: It explicitly states that it takes a string as input and returns a string.
    • Logging: The console log helps trace when the function is executed.

2. Print Message Function

const printMessage = (message: string) => {
console.log("LOG: printing the message");
console.log(message);
};
  • Purpose: This function takes a string and prints it to the console.
  • Key Points:
    • It logs a message before printing the actual message to indicate its operation.
    • There is no return value; its job is to output the message.

3. Get Message Function

let getMessage = (index: number): Promise<string> => {
return new Promise((resolve, reject) => {
const messages = ["Hello World", "Hello Universe", "Hello Multiverse"];
const durations = [1000, 500, 750];
const message = messages[index % messages.length];
const duration = durations[index % messages.length];
console.log(`LOG: getting the message[${index}]`);
setTimeout(() => {
console.log(`LOG: retrieved: "${message}"`);
resolve(message);
}, duration);
});
};
  • Purpose: This function simulates an asynchronous operation by returning a Promise that resolves to one of the predefined messages after a delay.
  • Key Points:
    • Message Selection: It uses the modulo operator (index % messages.length) to cycle through the messages and durations arrays. This means if you provide an index larger than the array length, it wraps around.
    • Simulated Delay: setTimeout is used to delay the execution, simulating an asynchronous operation. The delay is determined by the corresponding duration.
    • Promise Resolution: Once the timeout completes, the promise is resolved with the selected message. There is no error handling here (the reject is never used), so the promise always resolves.

4. Promise Chaining

getMessage(1)
.then((message) => upperCase(message))
.then((message) => printMessage(message));
// we could shorten getMessage call as below
// getMessage(1).then(upperCase).then(printMessage);
  • Execution Flow:
    1. Call to getMessage(1): The function is called with 1 as the index.
    2. First .then(): When the promise resolves, the retrieved message is passed to the upperCase function. This converts the message to uppercase.
    3. Second .then(): The uppercase message is then passed to the printMessage function, which prints it to the console.
  • Chaining Simplification: The commented line shows that you can directly pass the function names (upperCase and printMessage) to .then(), as they both match the expected signature.

Summary

  • Overall Design: The code demonstrates a simple asynchronous operation using Promises without async/await. It chains multiple functions to simulate a flow:
    1. Retrieve a message asynchronously (using getMessage).
    2. Transform the message (convert it to uppercase using upperCase).
    3. Display the message (print it using printMessage).
  • Key Concepts:
    • Promise chaining: Allows for sequential asynchronous operations.
    • TypeScript syntax: Shows type annotations for functions and variables.
    • Logging: Provides insight into the sequence of operations for debugging or tracking execution.

This modular approach helps in understanding and testing each part separately, making the code easier to maintain and debug.

Async/Await

async/await provides syntactic sugar over Promises, making asynchronous operations much more approachable.

async declares a function as asynchronous, implicitly returning a Promise. Inside an async function, await pauses execution until the Promise it’s waiting for resolves (or rejects), and then resumes execution with the resolved value.

This allows you to write asynchronous code that looks and behaves much more like synchronous code, making it significantly easier to read, understand, and debug.

Example 1

const printMessage = (message: string) => {
console.log(message);
};
const trueFalse = (): boolean => {
return Math.random() < 0.5;
};
const getMessagePromiseFull = (message: string) => {
return new Promise((resolve, reject) => {
setTimeout(() => {
trueFalse() ? resolve(message) : reject(new Error("Message Error"));
}, 1000);
});
};
const getMessages = async () => {
try {
const message1 = await getMessagePromiseFull("Promise Example Message 1");
console.log(message1);
const message2 = await getMessagePromiseFull("Promise Example Message 2");
console.log(message2);
} catch (err: any) {
console.log("Error:", err.message);
}
};
// usage
getMessages();

Output

Terminal window
# output
Error: Message Error
# output
Promise Example Message 1
Error: Message Error
# output
Promise Example Message 1
Promise Example Message 2

Explanation:

1. Function: printMessage

const printMessage = (message: string) => {
console.log(message);
};
  • A simple function that logs a given string message to the console.

2. Function: trueFalse

const trueFalse = (): boolean => {
return Math.random() < 0.5;
};
  • Generates a random boolean value.
  • Uses Math.random(), which produces a random float between 0 and 1.
  • If the random value is less than 0.5, it returns true; otherwise, it returns false.

3. Function: getMessagePromiseFull

const getMessagePromiseFull = (message: string) => {
return new Promise((resolve, reject) => {
setTimeout(() => {
trueFalse() ? resolve(message) : reject(new Error("Message Error"));
}, 1000);
});
};
  • Returns a Promise that either:
    • Resolves with the provided message if trueFalse() returns true.
    • Rejects with an Error object (new Error("Message Error")) if trueFalse() returns false.
  • The function simulates an asynchronous operation using setTimeout() with a 1-second delay.

4. Function: getMessages

const getMessages = async () => {
try {
const message1 = await getMessagePromiseFull("Promise Example Message 1");
console.log(message1);
const message2 = await getMessagePromiseFull("Promise Example Message 2");
console.log(message2);
} catch (err: any) {
console.log("Error:", err.message);
}
};
  • Declared as an async function, meaning it can use await inside it.
  • Calls getMessagePromiseFull("Promise Example Message 1") and waits (await) for it to resolve before proceeding.
  • If the first promise resolves, it logs the message and calls the second promise (getMessagePromiseFull("Promise Example Message 2")).
  • If either promise rejects, execution jumps to the catch block, where it logs "Error: Message Error".

5. Function Call: getMessages()

getMessages();
  • Initiates the getMessages function.
  • Depending on random outcomes (trueFalse()), it may:
    • Successfully log both messages.
    • Fail on the first message and log an error.
    • Fail on the second message and log an error.

Example Scenarios

Scenario 1: Both messages succeed

(Assume trueFalse() returns true both times)

Promise Example Message 1
Promise Example Message 2

Scenario 2: First message fails

(Assume trueFalse() returns false for the first message)

Error: Message Error

Scenario 3: Second message fails

(Assume trueFalse() returns true for the first message but false for the second)

Promise Example Message 1
Error: Message Error

Key Concepts Used

  1. Promises - Handling asynchronous operations (getMessagePromiseFull).
  2. Async/Await - Synchronous-style promise handling (getMessages).
  3. Randomized Behavior - trueFalse() introduces a 50% failure rate.
  4. Error Handling - try...catch ensures graceful failure handling.

This structure is useful for simulating unreliable APIs where responses might randomly fail.

Division Example

const printResult = (result: number) => {
console.log(result);
};
const printOddEven = (value: number) => {
value % 2 === 0
? console.log("Result is even")
: console.log("Result is odd");
};
const divide = (a: number, b: number): Promise<number> => {
return new Promise((resolve, reject) => {
setTimeout(() => {
if (b === 0) reject(new Error("Division Error!"));
resolve(a / b);
}, 1000);
});
};
const addOne = (a: number): Promise<number> => {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve(a + 1);
}, 1000);
});
};
const calculate = async (a: number, b: number) => {
try {
const division = await divide(a, b);
const result = await addOne(division);
printResult(result);
printOddEven(result);
} catch (err) {
console.log(err.message);
}
};
// usage
calculate(4, 2);

Output

Terminal window
# output
3
Result is odd

Division Example with Return

const printResult = (result: number) => {
console.log(result);
};
const printOddEven = (value: number) => {
value % 2 === 0
? console.log("Result is even")
: console.log("Result is odd");
};
const divide = (a: number, b: number): Promise<number> => {
return new Promise((resolve, reject) => {
setTimeout(() => {
if (b === 0) reject(new Error("Division Error!"));
resolve(a / b);
}, 1000);
});
};
const addOne = (a: number): Promise<number> => {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve(a + 1);
}, 1000);
});
};
const calculate = async (a: number, b: number): Promise<number> => {
const division = await divide(a, b);
const result = await addOne(division);
return result;
};
// usage
calculate(4, 2)
.then((result) => {
printResult(result);
printOddEven(result);
})
.catch((err) => console.log(err.message))
.finally(() => console.log("Operation Complete."));
Terminal window
# output
3
Result is odd
Operation Complete.

RxJS

RxJS (Reactive Extensions for JavaScript) is a library for composing asynchronous and event-based programs by using observable sequences. It provides a powerful and flexible way to handle asynchronous operations, such as network requests, user interactions, and more.

Observable

An Observable represents a stream of data or events over time.

import { Observable } from 'rxjs';
const observable = new Observable<number>((subscriber) => {
subscriber.next(1);
subscriber.next(2);
subscriber.next(3);
setTimeout(() => {
subscriber.next(4);
subscriber.complete();
}, 1000);
});

This code demonstrates the creation and behavior of an RxJS Observable, which is a core concept in Reactive Programming using the rxjs library.

Breaking Down the Code

1. Importing Observable

import { Observable } from 'rxjs';
  • Imports the Observable class from the rxjs library.
  • Observable represents a stream of data that can emit multiple values over time.

2. Creating an Observable

const observable = new Observable<number>((subscriber) => {
  • Defines an Observable that emits number values (Observable<number>).
  • The constructor takes a function with a subscriber parameter, which controls the emission of values.

3. Emitting Values Immediately

subscriber.next(1);
subscriber.next(2);
subscriber.next(3);
  • The subscriber.next(value) method emits values (1, 2, and 3) synchronously.
  • These values are sent immediately when a subscriber subscribes.

4. Asynchronous Emission with setTimeout

setTimeout(() => {
subscriber.next(4);
subscriber.complete();
}, 1000);
  • Delays the emission of 4 by 1 second using setTimeout().
  • After emitting 4, it calls subscriber.complete(), signaling that no more values will be emitted.
  • After complete(), no further emissions are possible.

Observing the Observable For this observable to actually do anything, a subscriber must subscribe to it:

observable.subscribe({
next: (value) => console.log(value), // Handles emitted values
complete: () => console.log("Done"), // Handles completion
});

Expected Output When subscribed, the output will be:

1
2
3
(1 second delay)
4
Done

Key Concepts in the Code

  1. Observable: Represents a stream of data over time.
  2. Synchronous Emission: 1, 2, and 3 are emitted immediately.
  3. Asynchronous Emission: 4 is emitted after 1 second.
  4. Completion (complete()): Notifies subscribers that the observable has finished.
  5. Lazy Execution: The observable doesn’t start emitting until someone subscribes.

Use Case Example This approach is useful in event-based or real-time data handling, such as:

  • Handling user input events.
  • Streaming real-time data (e.g., stock prices).
  • Managing API polling or delayed responses.

Subscriber

A Subscriber is an object that listens to an Observable and reacts to the values emitted by the Observable.

Here’s how you can subscribe to the observable and handle values, errors, and completion:

Subscribing to the Observable with Error Handling

import { Observable } from 'rxjs';
const observable = new Observable<number>((subscriber) => {
subscriber.next(1);
subscriber.next(2);
subscriber.next(3);
setTimeout(() => {
subscriber.next(4);
// Uncomment below to simulate an error
// subscriber.error(new Error("Something went wrong!"));
subscriber.complete();
}, 1000);
});
console.log("Subscribing to the observable...");
const subscription = observable.subscribe({
next: (value) => console.log("Received:", value), // Handles emitted values
error: (err) => console.error("Error:", err.message), // Handles errors
complete: () => console.log("Observable completed!") // Handles completion
});
// Unsubscribe after 2 seconds (optional)
setTimeout(() => {
console.log("Unsubscribing...");
subscription.unsubscribe();
}, 2000);

Explanation of Subscription

  1. The subscriber listens to the observable.
  2. When a value is emitted (next()), it logs "Received: X".
  3. If an error occurs (error()), it logs "Error: Something went wrong!".
  4. When the observable completes (complete()), it logs "Observable completed!".
  5. Unsubscribing after 2 seconds ensures cleanup to avoid memory leaks.

Expected Output

Subscribing to the observable...
Received: 1
Received: 2
Received: 3
(1 second delay)
Received: 4
Observable completed!
(After 2 seconds)
Unsubscribing...

Simulating an Error

If you uncomment:

subscriber.error(new Error("Something went wrong!"));

Then instead of completion, it will print:

Subscribing to the observable...
Received: 1
Received: 2
Received: 3
(1 second delay)
Error: Something went wrong!
  • The observable stops emitting after an error.
  • The complete() function will not execute after an error.

Key Takeaways

  • Observables emit values over time (next()).
  • They can fail (error()), stopping further emissions.
  • They can complete normally (complete()).
  • You can unsubscribe to stop listening early.

Multiple Subscribers

In RxJS, multiple subscribers can listen to the same observable, and each subscriber receives its own independent execution of the observable unless the observable is shared.

import { Observable } from 'rxjs';
const observable = new Observable<number>((subscriber) => {
console.log("Observable execution started");
subscriber.next(1);
subscriber.next(2);
setTimeout(() => {
subscriber.next(3);
subscriber.complete();
}, 1000);
});
// First Subscriber
observable.subscribe({
next: (value) => console.log("Subscriber 1 received:", value),
complete: () => console.log("Subscriber 1 completed!")
});
// Second Subscriber (subscribes after a delay)
setTimeout(() => {
observable.subscribe({
next: (value) => console.log("Subscriber 2 received:", value),
complete: () => console.log("Subscriber 2 completed!")
});
}, 500);

Explanation

  1. An observable is created that:
    • Emits 1 and 2 immediately.
    • Emits 3 after 1 second.
    • Calls complete(), signaling no further emissions.
  2. First Subscriber subscribes immediately and starts receiving values.
  3. Second Subscriber subscribes after 500ms, so:
    • It misses the first two emitted values (1 and 2).
    • It only gets values emitted after it subscribes.

Expected Output

Observable execution started
Subscriber 1 received: 1
Subscriber 1 received: 2
(500ms delay)
Observable execution started
Subscriber 2 received: 1
Subscriber 2 received: 2
(500ms more delay)
Subscriber 1 received: 3
Subscriber 1 completed!
Subscriber 2 received: 3
Subscriber 2 completed!

Note: Since it’s a cold observable (default behavior), each subscriber triggers the observable from the beginning.

Hot Observable (Shared Execution)

If you want all subscribers to share the same execution, use share() from RxJS:

import { Observable, share } from 'rxjs';
const sharedObservable = new Observable<number>((subscriber) => {
console.log("Shared Observable execution started");
subscriber.next(1);
subscriber.next(2);
setTimeout(() => {
subscriber.next(3);
subscriber.complete();
}, 1000);
}).pipe(share()); // Converts it into a hot observable
// First Subscriber
sharedObservable.subscribe({
next: (value) => console.log("Subscriber 1 received:", value),
complete: () => console.log("Subscriber 1 completed!")
});
// Second Subscriber (subscribes after 500ms)
setTimeout(() => {
sharedObservable.subscribe({
next: (value) => console.log("Subscriber 2 received:", value),
complete: () => console.log("Subscriber 2 completed!")
});
}, 500);

Hot vs Cold Observables

FeatureCold Observable (default)Hot Observable (share())
Execution per subscriberSeparate execution for eachShared execution
Late subscribersRestart from the beginningGet only new emissions
ExampleAPI calls, independent timersWebSocket, Live Data

Would you like a WebSocket example where all subscribers receive real-time updates? 🚀

Operators

Operators are functions that transform, filter, or combine Observables.

import { from, map, filter } from 'rxjs';
const numbers = from([1, 2, 3, 4, 5]);
const squaredEvenNumbers = numbers.pipe(
filter(x => x % 2 === 0),
map(x => x * x)
);
squaredEvenNumbers.subscribe(x => console.log('Squared Even Number:', x));

Output

Terminal window
# output
Squared Even Number: 4
Squared Even Number: 16

sources