JavaScript: From Browsers to Servers
JavaScript was created in 1995 by Brendan Eich for the Netscape browser to make websites more interactive. It started as a language for client-side tasks, running directly in the browser. Fast forward to 2009, Ryan Dahl introduced Node.js, taking JavaScript beyond the browser and into the server world. With Node.js, JavaScript could now handle things like APIs and file systems. At the heart of this revolution is the V8 engine, which powers both Chrome and Node.js. It’s super fast because it compiles JavaScript directly into machine code, making everything run smoothly and efficiently.
Synchronous vs. Asynchronous Behavior
JavaScript is single-threaded, meaning it can only handle one task at a time. But here’s the catch: it’s not naturally asynchronous. Instead, it achieves asynchronous behavior using tools like the event loop, Promises, async/await, and good old callbacks.
Other languages like Java and Python can also do async magic depending on how they’re set up. For example, Python has asyncio to handle asynchronous programming.
Now, what if you need to wait for some data before moving on? JavaScript gives you three main ways to handle this:
- Callback Functions: Pass a function as a parameter to another function to deal with the result.
- Promises: Use
.thenand.catchto handle results asynchronously with built-inresolveandrejectfunctions. - Async/Await: Write cleaner, more readable async code using
awaitto pause execution until the promise resolves.
Let’s dive into an example to see how these work!
Get Sathveegan Yogendrarajah’s stories in your inbox
Join Medium for free to get updates from this writer.
Example: Fetching Data from a URL
const fetchData = (url, callback) => {
setTimeout(() => {
console.log(`Fetched data from ${url}`);
callback({ data: 'Sample data' });
}, 1000);
};
// Callback Example
fetchData('https://example.com', (result) => {
console.log('Callback:', result.data);
});
// Promise Example
const fetchDataPromise = (url) => {
return new Promise((resolve, reject) => {
setTimeout(() => {
console.log(`Fetched data from ${url}`);
resolve({ data: 'Sample data' });
}, 1000);
});
};
fetchDataPromise('https://example.com')
.then((result) => console.log('Promise:', result.data))
.catch((error) => console.error(error));
// Async/Await Example
const fetchAsync = async (url) => {
try {
const result = await fetchDataPromise(url);
console.log('Async/Await:', result.data);
} catch (error) {
console.error(error);
}
};
fetchAsync('https://example.com');About Node.js
Node.js uses an event-driven, non-blocking I/O architecture, making it efficient for real-time applications and high-concurrency tasks.
Components:
- Single-Threaded Event Loop: Node.js uses one thread to handle all incoming requests asynchronously.
- V8 JavaScript Engine: Executes JavaScript code by compiling it into machine code for faster execution.
- Event Loop: Continuously checks for pending tasks and executes their associated callbacks when completed.
- Non-Blocking I/O: Handles I/O operations (e.g., file system, network) asynchronously without blocking the main thread.
- Libuv: A library that handles asynchronous I/O operations and manages the event loop.
- Thread Pool: Used for certain tasks (like file system operations) that cannot be executed asynchronously.
- Callback Functions: Used to handle results of asynchronous operations once they are completed.
Here’s the flow for I/O-bound → CPU-bound → I/O-bound tasks in Node.js:
- Request 1 (I/O-bound):The event loop detects the I/O task (e.g., database query) and sends it to the libuv thread pool for asynchronous processing.
- Request 2 (CPU-bound):The event loop directly handles the CPU-bound task (e.g., heavy computation) on the single thread, which may block the thread temporarily.
- Request 3 (I/O-bound):The event loop queues the second I/O-bound task (e.g., file read) and sends it to the thread pool while continuing to monitor for callbacks.
- I/O Task Completion:Once the first I/O task is completed, its callback is added to the event loop for execution.
- CPU Task Completion:After finishing the CPU-bound task, the event loop becomes free to handle other tasks.
- Second I/O Task Completion:The event loop processes the callback of the second I/O-bound task and finalizes it.
- Response Sent:Responses for all tasks are sent back to the respective clients once their processing is complete.





