Two Ways to Speak the Same Language: Rethinking Promise vs Async/Await in JavaScript
Promises and async/await represent the same asynchronous system in JavaScript, with Promises providing explicit chaining and async/await offering a more readable, sequential syntax built on top of Promise mechanics.
At some point, every JavaScript developer notices something curious.
There are two ways to write asynchronous code.
One looks like this:
fetchData()
.then(data => process(data))
.then(result => save(result))
.catch(handleError)And the other looks like this:
try {
const data = await fetchData()
const result = process(data)
await save(result)
} catch (err) {
handleError(err)
}They feel different.
One is chained.
The other is sequential.
One feels mechanical.
The other feels natural.
But underneath, they are doing the same thing.
Not Two SystemsâOne System
Itâs easy to think of Promises and async/await as alternatives.
But they are not.
async/await is built on top of PromisesIt doesnât replace them.
It reshapes how you interact with them.
Every await is, at its core, a .then().
Every async function returns a Promise.
The system hasnât changed.
Only the way you express it has.
The Shape of Thought
The real difference lies in how you think while writing code.
With Promises, you think in chains.
Each step transforms a value and passes it forward.
step â step â step â stepWith async/await, you think in sequence.
Do this.
Wait.
Then do the next thing.
do â wait â do â waitThe logic is identical.
But the mental model shifts.
Flow vs Structure
Promises emphasize structure.
They make the flow explicit.
You see exactly how values move from one step to the next.
This makes them powerful for pipelines and transformations.
But as complexity grows, the structure can become harder to follow.
Nested logic.
Multiple branches.
Error handling scattered across the chain.
And this is where async/await changes things.
Bringing Back Linearity
async/await restores a sense of linear flow.
It lets you write asynchronous code as if it were synchronous.
Not because it isâ
but because it reads that way.
const data = await fetchData()This line pauses execution.
But visually, it doesnât fragment the code.
It keeps the narrative intact.
Error Handling Feels Familiar
Another subtle shift happens with errors.
In Promise chains, errors flow downward:
.then(...)
.catch(...)In async/await, errors behave like synchronous ones:
try {
await something()
} catch (e) {
handle(e)
}This makes error handling feel more predictable.
Not because it changedâ
but because it now mirrors something you already understand.
The Hidden Trade-Off
While async/await improves readability, it introduces a quiet risk.
It can make asynchronous operations unintentionally sequential.
await fetchA()
await fetchB()This runs one after the other.
But with Promises:
await Promise.all([fetchA(), fetchB()])You regain parallelism.
This is where understanding the underlying system matters.
Because the syntax can hide what the code is actually doing.
The Same Timing, Different Expression
Both Promises and async/await rely on the same mechanics:
- the microtask queue
- the event loop
- the resolution procedure
Nothing about execution timing changes.
Only how you write the logic.
A Subtle Relationship
Instead of thinking:
âWhich one should I use?â
It becomes more useful to think:
âWhat kind of flow am I expressing?â
If your logic is a transformation pipeline, Promises feel natural.
If your logic is step-by-step and conditional, async/await feels clearer.
They are not competing tools.
They are different lenses over the same system.
A Final Thought
Promises reveal how asynchronous code works.
async/await lets you write it without thinking about the machinery.
One exposes the system.
The other softens it.
And understanding both is what gives you controlâ
not just over what your code does,
but over how clearly it expresses it.