Most developers learn map() and think they've figured it out.
You take a value.
Transform it.
Get a new value.
Simple.
const doubled = [1, 2, 3]
.map(x => x * 2)
console.log(doubled)
// [2, 4, 6]
Life is good.
Until one day your transformation returns another container.
And suddenly everything breaks.
You start seeing:
[
[1, 10],
[2, 20],
[3, 30]
]
instead of:
[1, 10, 2, 20, 3, 30]
Or:
Promise<Promise<User>>
instead of:
Promise<User>
Or nested Observables.
Or nested async workflows.
Or nested arrays.
Or nested state.
At that moment, you discover one of the most important abstractions in programming:
FlatMap.
And once you understand FlatMap, a lot of modern JavaScript suddenly makes sense.
The Problem With map()
Let's start with something innocent.
const numbers = [1, 2, 3]
const result = numbers.map(
x => [x, x * 10]
)
console.log(result)
Output:
[
[1, 10],
[2, 20],
[3, 30]
]
This is correct.
But what if we actually wanted:
[
1, 10,
2, 20,
3, 30
]
The problem is:
map()
transformed each value
but preserved the container
Remember from the previous article:
Container<Value>
↓
Transform
↓
Container<NewValue>
The issue is that our transformation itself returned a container.
x => [x, x * 10]
Now we have:
Array
↓
map
↓
Array<Array>
Nested containers.
Enter flatMap()
JavaScript gives us:
const result = [1, 2, 3]
.flatMap(
x => [x, x * 10]
)
console.log(result)
Output:
[
1, 10,
2, 20,
3, 30
]
Exactly what we wanted.
Why?
Because FlatMap does two operations.
map
+
flatten
Hence the name:
FlatMap
Visualizing The Difference
Normal map:
[1,2,3]
↓
[
[1,10],
[2,20],
[3,30]
]
FlatMap:
[1,2,3]
↓
[
1,10,
2,20,
3,30
]
Same transformation.
Different result.
Why This Matters More Than Arrays
Most developers stop here.
That is a mistake.
Arrays are only the beginning.
The real importance of FlatMap appears when we look at Promises.
Promise.then() Is Basically FlatMap
Consider:
Promise.resolve(10)
.then(x => x * 2)
Easy.
Now:
Promise.resolve(10)
.then(x => {
return Promise.resolve(
x * 2
)
})
What should happen?
If Promise behaved like Array.map(), we would get:
Promise<Promise<number>>
That would be awful.
Instead we get:
Promise<number>
because Promises automatically flatten.
Which means:
Promise.then()
behaves like FlatMap
Not map.
FlatMap.
This is why async code feels natural.
Promises remove nesting automatically.
The Callback Hell Problem
Before Promises:
getUser(id, user => {
getOrders(user.id, orders => {
getPayments(
orders,
payments => {
...
}
)
})
})
Deep nesting.
Hard to read.
Hard to maintain.
Promises solve this because they flatten.
getUser(id)
.then(user =>
getOrders(user.id)
)
.then(orders =>
getPayments(orders)
)
Flat structure.
Cleaner code.
That flattening behavior is FlatMap.
RxJS Makes This Explicit
RxJS has multiple versions:
mergeMap()
switchMap()
concatMap()
exhaustMap()
Notice something.
They all contain:
Map
Because they transform values.
And they all solve:
Nested Observables
Problem.
Example:
searchText$
.pipe(
switchMap(
text => api.search(text)
)
)
Without switchMap:
Observable<
Observable<SearchResult>
>
With switchMap:
Observable<SearchResult>
Again:
Map
+
Flatten
FlatMap.
Real World Example: User Permissions
Suppose:
const users = [
{
name: "John",
permissions: [
"read",
"write"
]
},
{
name: "Sarah",
permissions: [
"read",
"delete"
]
}
]
Using map:
const permissions =
users.map(
user => user.permissions
)
Output:
[
["read", "write"],
["read", "delete"]
]
Using flatMap:
const permissions =
users.flatMap(
user => user.permissions
)
Output:
[
"read",
"write",
"read",
"delete"
]
Often more useful.
Real World Example: API Aggregation
Suppose each team contains members.
const teams = [
{
name: "Frontend",
members: ["John", "Sarah"]
},
{
name: "Backend",
members: ["Ahmed"]
}
]
Map:
teams.map(
team => team.members
)
Result:
[
["John", "Sarah"],
["Ahmed"]
]
FlatMap:
teams.flatMap(
team => team.members
)
Result:
[
"John",
"Sarah",
"Ahmed"
]
This pattern appears constantly in business applications.
FlatMap Is Really About Composition
The deeper idea isn't flattening.
The deeper idea is composition.
Imagine:
User
↓
fetchOrders
↓
Orders
Then:
Orders
↓
fetchPayments
↓
Payments
Without FlatMap:
Nested structures
With FlatMap:
Single pipeline
That is why FlatMap became one of the most important abstractions in software.
It allows computations that return containers to compose naturally.
The Hidden Relationship Between map() and FlatMap
Map:
Value
↓
Transform
↓
Value
FlatMap:
Value
↓
Transform
↓
Container<Value>
↓
Flatten
FlatMap handles the extra layer.
That is its entire purpose.
Performance Considerations
FlatMap is often more efficient than:
array
.map(...)
.flat()
because JavaScript can perform both operations together.
Instead of:
Map
↓
Allocate Array
↓
Flatten
it can combine the work.
Always benchmark for critical code paths.
But generally:
flatMap()
is preferable when that is exactly what you intend.
When Not To Use FlatMap
Sometimes nesting is meaningful.
Example:
const teamMembers =
teams.map(
team => team.members
)
Result:
[
["John", "Sarah"],
["Ahmed"]
]
This preserves team boundaries.
Flattening would destroy that information.
So ask yourself:
Do I want hierarchy?
or
Do I want a single collection?
Choose accordingly.
Pros Of FlatMap
1. Eliminates Nested Structures
Avoids:
Array<Array<T>>
Promise<Promise<T>>
Observable<Observable<T>>
2. Improves Composition
Workflows become linear.
3. Cleaner Async Code
Promises rely heavily on FlatMap behavior.
4. Reduces Boilerplate
Less manual flattening.
5. Common Across Ecosystems
Arrays.
Promises.
RxJS.
Streams.
Functional libraries.
Cons Of FlatMap
1. Can Hide Complexity
Nested structures sometimes carry important meaning.
2. Overuse Can Reduce Clarity
Flattening everything is not always correct.
3. Some Developers Find It Less Intuitive
Especially when learning functional concepts.
4. Different Libraries Flatten Differently
RxJS operators have distinct behaviors.
Understanding them takes time.
5. Easy To Misuse
Flattening data that should remain hierarchical can create bugs.
The Real Lesson
Most developers think FlatMap exists to flatten arrays.
That is technically true.
But it is far too shallow.
The real purpose of FlatMap is:
Allowing computations that return containers to compose naturally.
That is why:
Array.flatMap()
exists.
That is why:
Promise.then()
works the way it does.
That is why:
switchMap()
mergeMap()
concatMap()
exist in RxJS.
The moment you understand FlatMap, you stop seeing it as an array utility.
You start seeing it as a composition tool.
And once you see that, modern JavaScript becomes much easier to understand.
What's Next?
In the next article we'll discuss:
You've Been Using Monads Without Realizing It
Because once you understand:
Functor
↓
Map
FlatMap
↓
Composition
you're already 90% of the way to understanding Monads.
The funny part?
You've probably been using them for years.
About The Author
Hi, I'm Amrish Khan.
I enjoy building developer tools, exploring software architecture, and writing about the deeper ideas behind everyday programming concepts.
I'm also building Aruvix — a growing ecosystem of local-first developer tools designed to process data directly in the browser without unnecessary uploads.
Here's a detailed blog on Aruvix:
https://dev.to/amrishkhan05/aruvix-the-ultimate-offline-first-developer-toolkit-e0i
You can follow my work and thoughts here:
Portfolio:
https://www.amrishkhan.dev
LinkedIn:
https://www.linkedin.com/in/amrishkhan
GitHub:
https://www.github.com/amrishkhan05
If you enjoyed this article, consider following for more deep dives into JavaScript, architecture, local-first software, and performance engineering.
Top comments (0)