DEV Community

Cover image for FlatMap Is More Important Than You Think
Amrishkhan Sheik Abdullah
Amrishkhan Sheik Abdullah

Posted on

FlatMap Is More Important Than You Think

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]
Enter fullscreen mode Exit fullscreen mode

Life is good.

Until one day your transformation returns another container.

And suddenly everything breaks.

You start seeing:

[
  [1, 10],
  [2, 20],
  [3, 30]
]
Enter fullscreen mode Exit fullscreen mode

instead of:

[1, 10, 2, 20, 3, 30]
Enter fullscreen mode Exit fullscreen mode

Or:

Promise<Promise<User>>
Enter fullscreen mode Exit fullscreen mode

instead of:

Promise<User>
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

Output:

[
  [1, 10],
  [2, 20],
  [3, 30]
]
Enter fullscreen mode Exit fullscreen mode

This is correct.

But what if we actually wanted:

[
  1, 10,
  2, 20,
  3, 30
]
Enter fullscreen mode Exit fullscreen mode

The problem is:

map()
transformed each value

but preserved the container
Enter fullscreen mode Exit fullscreen mode

Remember from the previous article:

Container<Value>
↓
Transform
↓
Container<NewValue>
Enter fullscreen mode Exit fullscreen mode

The issue is that our transformation itself returned a container.

x => [x, x * 10]
Enter fullscreen mode Exit fullscreen mode

Now we have:

Array
↓
map
↓
Array<Array>
Enter fullscreen mode Exit fullscreen mode

Nested containers.


Enter flatMap()

JavaScript gives us:

const result = [1, 2, 3]
  .flatMap(
    x => [x, x * 10]
  )

console.log(result)
Enter fullscreen mode Exit fullscreen mode

Output:

[
  1, 10,
  2, 20,
  3, 30
]
Enter fullscreen mode Exit fullscreen mode

Exactly what we wanted.

Why?

Because FlatMap does two operations.

map
+
flatten
Enter fullscreen mode Exit fullscreen mode

Hence the name:

FlatMap
Enter fullscreen mode Exit fullscreen mode

Visualizing The Difference

Normal map:

[1,2,3]

↓

[
  [1,10],
  [2,20],
  [3,30]
]
Enter fullscreen mode Exit fullscreen mode

FlatMap:

[1,2,3]

↓

[
  1,10,
  2,20,
  3,30
]
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

Easy.

Now:

Promise.resolve(10)
  .then(x => {
    return Promise.resolve(
      x * 2
    )
  })
Enter fullscreen mode Exit fullscreen mode

What should happen?

If Promise behaved like Array.map(), we would get:

Promise<Promise<number>>
Enter fullscreen mode Exit fullscreen mode

That would be awful.

Instead we get:

Promise<number>
Enter fullscreen mode Exit fullscreen mode

because Promises automatically flatten.

Which means:

Promise.then()
behaves like FlatMap
Enter fullscreen mode Exit fullscreen mode

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 => {
        ...
      }
    )
  })
})
Enter fullscreen mode Exit fullscreen mode

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)
  )
Enter fullscreen mode Exit fullscreen mode

Flat structure.

Cleaner code.

That flattening behavior is FlatMap.


RxJS Makes This Explicit

RxJS has multiple versions:

mergeMap()
switchMap()
concatMap()
exhaustMap()
Enter fullscreen mode Exit fullscreen mode

Notice something.

They all contain:

Map
Enter fullscreen mode Exit fullscreen mode

Because they transform values.

And they all solve:

Nested Observables
Enter fullscreen mode Exit fullscreen mode

Problem.

Example:

searchText$
  .pipe(
    switchMap(
      text => api.search(text)
    )
  )
Enter fullscreen mode Exit fullscreen mode

Without switchMap:

Observable<
  Observable<SearchResult>
>
Enter fullscreen mode Exit fullscreen mode

With switchMap:

Observable<SearchResult>
Enter fullscreen mode Exit fullscreen mode

Again:

Map
+
Flatten
Enter fullscreen mode Exit fullscreen mode

FlatMap.


Real World Example: User Permissions

Suppose:

const users = [
  {
    name: "John",
    permissions: [
      "read",
      "write"
    ]
  },
  {
    name: "Sarah",
    permissions: [
      "read",
      "delete"
    ]
  }
]
Enter fullscreen mode Exit fullscreen mode

Using map:

const permissions =
  users.map(
    user => user.permissions
  )
Enter fullscreen mode Exit fullscreen mode

Output:

[
  ["read", "write"],
  ["read", "delete"]
]
Enter fullscreen mode Exit fullscreen mode

Using flatMap:

const permissions =
  users.flatMap(
    user => user.permissions
  )
Enter fullscreen mode Exit fullscreen mode

Output:

[
  "read",
  "write",
  "read",
  "delete"
]
Enter fullscreen mode Exit fullscreen mode

Often more useful.


Real World Example: API Aggregation

Suppose each team contains members.

const teams = [
  {
    name: "Frontend",
    members: ["John", "Sarah"]
  },
  {
    name: "Backend",
    members: ["Ahmed"]
  }
]
Enter fullscreen mode Exit fullscreen mode

Map:

teams.map(
  team => team.members
)
Enter fullscreen mode Exit fullscreen mode

Result:

[
  ["John", "Sarah"],
  ["Ahmed"]
]
Enter fullscreen mode Exit fullscreen mode

FlatMap:

teams.flatMap(
  team => team.members
)
Enter fullscreen mode Exit fullscreen mode

Result:

[
  "John",
  "Sarah",
  "Ahmed"
]
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Then:

Orders

fetchPayments

Payments
Enter fullscreen mode Exit fullscreen mode

Without FlatMap:

Nested structures
Enter fullscreen mode Exit fullscreen mode

With FlatMap:

Single pipeline
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

FlatMap:

Value
↓
Transform
↓
Container<Value>
↓
Flatten
Enter fullscreen mode Exit fullscreen mode

FlatMap handles the extra layer.

That is its entire purpose.


Performance Considerations

FlatMap is often more efficient than:

array
  .map(...)
  .flat()
Enter fullscreen mode Exit fullscreen mode

because JavaScript can perform both operations together.

Instead of:

Map
↓
Allocate Array
↓
Flatten
Enter fullscreen mode Exit fullscreen mode

it can combine the work.

Always benchmark for critical code paths.

But generally:

flatMap()
Enter fullscreen mode Exit fullscreen mode

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
  )
Enter fullscreen mode Exit fullscreen mode

Result:

[
  ["John", "Sarah"],
  ["Ahmed"]
]
Enter fullscreen mode Exit fullscreen mode

This preserves team boundaries.

Flattening would destroy that information.

So ask yourself:

Do I want hierarchy?

or

Do I want a single collection?
Enter fullscreen mode Exit fullscreen mode

Choose accordingly.


Pros Of FlatMap

1. Eliminates Nested Structures

Avoids:

Array<Array<T>>
Promise<Promise<T>>
Observable<Observable<T>>
Enter fullscreen mode Exit fullscreen mode

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()
Enter fullscreen mode Exit fullscreen mode

exists.

That is why:

Promise.then()
Enter fullscreen mode Exit fullscreen mode

works the way it does.

That is why:

switchMap()
mergeMap()
concatMap()
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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)