
Best Async Document for Typescript
/ 25 min read
Updated:Table of Contents
JavaScript Engine & Runtime Environment
Javascript code runs in a JavaScript engine provided by the runtime environment.
Runtime Environment | JavaScript Engine |
---|---|
Node.js | V8 |
Deno | V8 |
Bun | JavaScriptCore |
Firefox | SpiderMonkey |
Chrome | V8 |
Edge | V8 |
Safari | JavaScriptCore |
IE | ChakraCore |
Oracle | Nashorn(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 taskconst response = await axios.get(yourUrl)
is a tasksetTimeout(function, 100)
is a taskconst 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
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
hello world 1hello world 2hello world 3hello world 4hello 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
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
StartRan for 2000 millisecondsFinish
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 secondsfreeze(2000); // second task running for 2 secondsconst finish = Date.now();console.log(`Ran for ${finish - start} milliseconds`);console.log("Finish");
Output
StartRan for 4000 millisecondsFinish
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
StartRan for 0 millisecondsFinishHello 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
StartRan for 0 millisecondsFinishHello 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 functionconst getMessage = () => { console.log("getting the message"); setTimeout(() => { console.log("retrieved the message"); }, 1000);};
// not an async functionconst printMessage = () => { console.log("printing message");};
getMessage();printMessage();
Output
getting the messageprinting the messageretrieved 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
getting the messageprinting the messageretrieved 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
getting the messageretrieved the messageprinting 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
getting the messageretrieved the messageprinting 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
LOG: getting the messageLOG: retrieved: "Hello World"LOG: printing the messageHello 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
LOG: getting the message[0]LOG: getting the message[1]LOG: retrieved: "Hello Universe"LOG: converting to upper caseLOG: printing the messageHELLO UNIVERSELOG: retrieved: "Hello World"LOG: converting to upper caseLOG: printing the messageHELLO 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
LOG: getting the message[0]LOG: retrieved: "Hello World"LOG: converting to upper caseLOG: printing the messageHELLO WORLDLOG: getting the message[1]LOG: retrieved: "Hello Universe"LOG: converting to upper caseLOG: printing the messageHELLO 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
LOG: getting the message[0]LOG: retrieved: "Hello World"LOG: converting to upper caseLOG: printing the messageHELLO WORLDLOG: getting the message[1]LOG: retrieved: "Hello Universe"LOG: converting to upper caseLOG: printing the messageHELLO UNIVERSELOG: getting the message[2]LOG: retrieved: "Hello Multiverse"LOG: converting to upper caseLOG: printing the messageHELLO 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 Promisefunction 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 thetoUpperCase()
method, and returns the uppercase version. - Key Points:
- Type Annotations: It explicitly states that it takes a
string
as input and returns astring
. - Logging: The console log helps trace when the function is executed.
- Type Annotations: It explicitly states that it takes a
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 themessages
anddurations
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 correspondingduration
. - 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.
- Message Selection: It uses the modulo operator (
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:
- Call to
getMessage(1)
: The function is called with1
as the index. - First
.then()
: When the promise resolves, the retrieved message is passed to theupperCase
function. This converts the message to uppercase. - Second
.then()
: The uppercase message is then passed to theprintMessage
function, which prints it to the console.
- Call to
- Chaining Simplification: The commented line shows that you can directly pass the function names (
upperCase
andprintMessage
) 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:
- Retrieve a message asynchronously (using
getMessage
). - Transform the message (convert it to uppercase using
upperCase
). - Display the message (print it using
printMessage
).
- Retrieve a message asynchronously (using
- 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); }};
// usagegetMessages();
Output
# outputError: Message Error
# outputPromise Example Message 1Error: Message Error
# outputPromise Example Message 1Promise 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 between0
and1
. - If the random value is less than
0.5
, it returnstrue
; otherwise, it returnsfalse
.
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
iftrueFalse()
returnstrue
. - Rejects with an
Error
object (new Error("Message Error")
) iftrueFalse()
returnsfalse
.
- Resolves with the provided
- 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 1Promise 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 1Error: Message Error
Key Concepts Used
- Promises - Handling asynchronous operations (
getMessagePromiseFull
). - Async/Await - Synchronous-style promise handling (
getMessages
). - Randomized Behavior -
trueFalse()
introduces a 50% failure rate. - 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); }};
// usagecalculate(4, 2);
Output
# output3Result 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;};
// usagecalculate(4, 2) .then((result) => { printResult(result); printOddEven(result); }) .catch((err) => console.log(err.message)) .finally(() => console.log("Operation Complete."));
# output3Result is oddOperation 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 therxjs
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
, and3
) 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 usingsetTimeout()
. - After emitting
4
, it callssubscriber.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:
123(1 second delay)4Done
Key Concepts in the Code
- Observable: Represents a stream of data over time.
- Synchronous Emission:
1
,2
, and3
are emitted immediately. - Asynchronous Emission:
4
is emitted after 1 second. - Completion (
complete()
): Notifies subscribers that the observable has finished. - 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
- The subscriber listens to the observable.
- When a value is emitted (
next()
), it logs"Received: X"
. - If an error occurs (
error()
), it logs"Error: Something went wrong!"
. - When the observable completes (
complete()
), it logs"Observable completed!"
. - Unsubscribing after 2 seconds ensures cleanup to avoid memory leaks.
Expected Output
Subscribing to the observable...Received: 1Received: 2Received: 3(1 second delay)Received: 4Observable 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: 1Received: 2Received: 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 Subscriberobservable.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
- An observable is created that:
- Emits
1
and2
immediately. - Emits
3
after 1 second. - Calls
complete()
, signaling no further emissions.
- Emits
- First Subscriber subscribes immediately and starts receiving values.
- Second Subscriber subscribes after 500ms, so:
- It misses the first two emitted values (
1
and2
). - It only gets values emitted after it subscribes.
- It misses the first two emitted values (
Expected Output
Observable execution startedSubscriber 1 received: 1Subscriber 1 received: 2(500ms delay)Observable execution startedSubscriber 2 received: 1Subscriber 2 received: 2(500ms more delay)Subscriber 1 received: 3Subscriber 1 completed!Subscriber 2 received: 3Subscriber 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 SubscribersharedObservable.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
Feature | Cold Observable (default) | Hot Observable (share() ) |
---|---|---|
Execution per subscriber | Separate execution for each | Shared execution |
Late subscribers | Restart from the beginning | Get only new emissions |
Example | API calls, independent timers | WebSocket, 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
# outputSquared Even Number: 4Squared Even Number: 16
sources
- https://academind.com/tutorials/callbacks-vs-promises-vs-rxjs-vs-async-awaits/
- https://javascript.info/settimeout-setinterval
- https://www.javascripttutorial.net/javascript-bom/javascript-settimeout
- https://blog.bitsrc.io/javascript-internals-javascript-engine-run-time-environment-settimeout-web-api-eeed263b1617
- blocking vs non-blocking
- event loop video
- two queues, micro vs macro
- macro and micro tasks
- loop demo
- Promises, Async/Await
- Micro and Macro tasks~
- How does JavaScript and JavaScript engine work in the browser and node?