Not Many ThreadsâBut Many Moments: Understanding Concurrency in JavaScript
JavaScript handles concurrency not by running code in parallel but by using the event loop to schedule and interleave asynchronous tasks, allowing multiple operations to progress without blocking the single-threaded execution model.
At first, JavaScript seems limited.
It runs on a single thread.
One thing at a time.
One call stack.
And yet, it can:
- fetch data
- respond to user input
- animate UI
- handle multiple requests
All without freezing.
It feels like many things are happening at once.
But they arenât.
At leastânot in the way you might expect.
The Illusion of âAt the Same Timeâ
JavaScript does not run multiple pieces of code simultaneously.
It doesnât split execution across threads.
Instead, it creates the illusion of concurrency.
Not by doing moreâ
but by deciding when to do things.
The Role of the Event Loop
At the center of this illusion is the event loop.
There is always a single flow:
- execute whatâs on the call stack
- take the next task from the queue
- execute again
And repeat.
But while JavaScript is busy with one task, other operationsâlike network requestsâare handled elsewhere.
When they finish, they donât interrupt.
They wait.
They enter a queue.
And only when JavaScript is ready does it pick them up.
Not All Tasks Are Equal
Some tasks are treated with higher priority.
Promises, for example, are handled before timers.
This creates a subtle ordering:
- synchronous code runs first
- microtasks (like
Promise.then) run next - macrotasks (like
setTimeout) run after
This ordering is what makes the system predictable.
And sometimes surprising.
Concurrency Is About Coordination
Because JavaScript doesnât run things in parallel, concurrency becomes a matter of coordination.
Consider two requests:
await fetchA()
await fetchB()This is sequential.
One waits for the other.
But if you start them together:
const a = fetchA()
const b = fetchB()
await a
await bNow they overlap.
Not because JavaScript is running them at the same timeâ
but because it doesnât wait before moving on.
The difference is not in execution.
Itâs in timing.
When Timing Becomes a Problem
This is where things get interesting.
If two asynchronous operations finish in an unexpected order, you get a race condition.
An older request might overwrite newer data.
The system doesnât fail.
It just becomes inconsistent.
And the fix is not to make things fasterâ
but to make them coherent.
To decide which result should matter.
Blocking: The Enemy of Concurrency
Because everything runs on one thread, blocking that thread stops everything.
A long loop.
A heavy calculation.
And suddenly:
- no UI updates
- no event handling
- no progress
Concurrency depends on one principle:
Never block the flow
Controlling the Flow
Sometimes, you donât want everything to run freely.
You want control.
- limit how many requests run at once
- delay execution until input stabilizes
- prevent repeated triggers
This is where patterns like throttling and debouncing appear.
They donât make things faster.
They make them manageable.
A Different Perspective
It helps to stop thinking of concurrency as:
âdoing many things at onceâ
And start thinking of it as:
âdeciding what gets to run next, and whenâ
JavaScript is not parallel.
It is scheduled.
Where It All Connects
Youâve seen this idea before.
- Vue schedules updates
- the browser schedules rendering
- the event loop schedules execution
Different layers.
Same idea.
Everything is about timing.
A Final Thought
JavaScript doesnât gain power by splitting into many threads.
It gains power by staying single-threadedâ
and becoming extremely good at managing time.
What feels like concurrency is not simultaneous execution.
Itâs carefully ordered progress.
And once you see that,
you stop asking:
âHow do I run things at the same time?â
And start asking:
âHow do I let things happen without getting in each otherâs way?â