Revise JavaScript #02
In this part, we’ll explore Synchronous, Event Loop, Callback Queue, Job Queue, Asynchronous, and Threads in JavaScript with simple explanations and examples to strengthen your fundamentals.
Synchronous
So, what is the issue with being a single-threaded language? Let's solve this. When you visit a web page, you run a browser to do so. Each browser has its own version of JavaScript Runtime with a set of Web API's, methods that developers can access from the window object. In a synchronous language, only one thing can be done at a time.
Imagine an alert on the page, blocking the user from accessing any part of the page until the OK button is clicked. If everything in JavaScript that took a significant amount of time, blocked the browser, then we would have a pretty bad user experience. This is where concurrency and the event loop come in.
Event Loop and Callback Queue
When you run some js code in a browser, the engine starts to parse the code. Each line is executed and popped on and off the call stack. But what about Web API's? Web API's are not something JS recognizes, so the parser knows to pass it off to the browser for it to handle. When the browser is finished running its method, it puts what is needed to be ran by JavaScript into the callback queue. The callback queue cannot be run until the call stack is completely empty. So, the event loop is constantly checking the call stack to see if it is empty so that it can add anything in the callback queue back into the call stack. And finally, once it is back in the call stack, it is run and then popped off the stack.
console.log("1");
// goes on call stack and runs 1
setTimeout(() => {
console.log("2"), 1000;
});
// gets sent to web api
// web api waits 1 sec, runs and sends to callback queue
// the javascript engine keeps going
console.log("3");
// goes on call stack and runs 3
// event loop keeps checking and see call stack is empty
// event loop sends calback queue into call stack
// 2 is now ran
// 1
// 3
// 2
// Example with 0 second timeout
console.log("1");
setTimeout(() => {
console.log("2"), 0;
});
console.log("3");
// 1
// 3
// 2
// Still has the same outputIn the last example, we get the same output. How does this work if it waits 0 seconds? The JavaScript engine will still send off the setTimeout() to the Web API to be ran and it will then go into the callback queue and wait until the call stack is empty to be ran. So, we end up with the exact same endpoint.
Job Queue
The job queue or microtask queue came about with promises in ES6. The JavaScript engine is going to check the job queue before the callback queue.
// 1 Callback Queue ~ Task Queue
setTimeout(() => {
console.log("1", "is the loneliest number");
}, 0);
setTimeout(() => {
console.log("2", "can be as bad as one");
}, 10);
// 2 Job Queue ~ Microtask Queue
Promise.resolve("hi").then(data => console.log("2", data));
// 3
console.log("3", "is a crowd");
// 3 is a crowd
// 2 hi
// undefined Promise resolved
// 1 is the loneliest number
// 2 can be as bad as one3 Ways to Promise
There are 3 ways you could want promises to resolve, parallel (all together), sequential (1 after another), or a race (doesn't matter who wins).
const promisify = (item, delay) =>
new Promise(resolve => setTimeout(() => resolve(item), delay));
const a = () => promisify("a", 100);
const b = () => promisify("b", 5000);
const c = () => promisify("c", 3000);
async function parallel() {
const promises = [a(), b(), c()];
const [output1, output2, output3] = await Promise.all(promises);
return `parallel is done: ${output1} ${output2} ${output3}`;
}
async function sequence() {
const output1 = await a();
const output2 = await b();
const output3 = await c();
return `sequence is done: ${output1} ${output2} ${output3}`;
}
async function race() {
const promises = [a(), b(), c()];
const output1 = await Promise.race(promises);
return `race is done: ${output1}`;
}
sequence().then(console.log);
parallel().then(console.log);
race().then(console.log);0ms: sequence() starts
0ms: parallel() starts
0ms: race() starts
100ms: ✅ race done → "race is done: a"
3000ms: c finished (but parallel still waiting for b)
5000ms: ✅ parallel done → "parallel is done: a b c"
8100ms: ✅ sequence done → "sequence is done: a b c"
// final output
race is done: a
parallel is done: a b c
sequence is done: a b c
all three functions start almost at the same time, and none of them blocks the main thread. This is asynchronous because JavaScript starts the tasks and continues running without waiting, and the results come later using Promises + Event Loop
Asynchronous
So, how does JavaScript handle long tasks while being a single-threaded language? The answer is asynchronous behavior.
In asynchronous programming, JavaScript does not wait for these tasks to finish. Instead, it sends them to the browser’s Web APIs and continues executing the rest of the code.
For example, if your page makes a network request to load data, JavaScript doesn’t freeze the entire UI. The request runs in the background, and once it finishes, the browser sends the result back to JavaScript through the callback queue. Then the event loop checks if the call stack is empty. If it is, it pushes the waiting callback into the stack so JavaScript can execute it. This is why asynchronous code improves performance and user experience, because the browser remains responsive while JavaScript waits for slow operations to complete.
Threads, Concurrency, and Parallelism
Even though JavaScript is a single threaded language, there are worker threads that work in the background that don't block the main thread. Just like a browser creates a new thread when you open a new tab. The workers work through messages being sent, but don't have access to the full program.
https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API/Using_web_workers
https://www.freecodecamp.org/news/scaling-node-js-applications-8492bd8afadc/
https://www.internalpointers.com/post/gentle-introduction-multithreading
var worker = new Worker("worker.js");
worker.postMessage("Helloooo");
addEventListener("message");Category
JavaScript