Microfrontends

I Solved Async Microfrontends with 1 Simple Tool (2025)

Tired of loading states, error handling, and communication chaos in your async microfrontends? Discover the simple tool that solves these problems for good.

A

Alex Ivanov

Principal Frontend Engineer specializing in scalable architecture and design systems.

7 min read15 views

We’ve all been there. You’ve successfully pitched and implemented a microfrontend architecture. Your teams are shipping independently, the monolith is shrinking, and for a moment, all is right with the world. Then, the other shoe drops: asynchronicity.

The Async Nightmare We All Pretend Is Fine

Microfrontends (MFEs) are fundamentally distributed systems running in a single client. When you load them asynchronously—which you should for performance—you invite a host of distributed systems problems. For years, my teams wrestled with this. We built brittle, bespoke solutions for what felt like solved problems, and it was maddening.

The Loading State Domino Effect

Your shell application loads. It needs to render three MFEs: `product-details`, `reviews`, and `recommendations`. `product-details` loads fast. `reviews` is a bit slower, so you show a spinner. But `recommendations` fails entirely because of a network blip. What happens now? Does the whole page show an error? Does the `reviews` spinner spin forever? How do you coordinate these states without writing a state machine that’s more complex than the MFEs themselves?

The Communication Black Hole

Okay, all three MFEs load. Now, the user adds a product to their cart from the `recommendations` MFE. The `mini-cart` MFE (in the header) needs to update. How does it know? You could use a global event bus, but who owns it? What if one MFE emits an event before the other is loaded and listening? You start littering your code with `setTimeout` and `window.postMessage` hacks, and a little piece of your soul dies.

The Boilerplate Swamp

For every single async MFE, we were writing the same logic: create a container, start a dynamic `import()`, handle the promise, render a loading state, catch the error, render a fallback UI, and finally, mount the component. Multiply this by dozens of MFEs across multiple teams, and you’re drowning in slightly different, bug-prone versions of the same code.

Advertisement

How We Used to Do It (And Why It Hurt)

We tried everything. Manual dynamic imports, full-blown solutions like Webpack Module Federation, and orchestration libraries. Each had its trade-offs, and none felt *simple*. The complexity was always leaking out somewhere.

Here’s a quick comparison of the common approaches:

FeatureManual Dynamic ImportWebpack Module FederationAsyncFuse (The New Way)
Setup ComplexityLowHighVery Low
Boilerplate CodeHigh (for state mgmt)Medium (webpack config)Minimal (declarative)
Built-in State HandlingNoneNone (DIY)✅ Yes (Loading/Error/Timeout)
Cross-MFE CommsManual (EventBus, etc.)Shared Modules✅ Yes (Built-in Bus)
Framework AgnosticYesYes, but complexYes

Module Federation is incredibly powerful, but it felt like using a sledgehammer to crack a nut for our use case. We didn't need shared library de-duplication as much as we needed robust, simple, async orchestration. The cognitive overhead of configuring remotes, hosts, and shared scopes was a constant drag on developer velocity.

The 'Aha!' Moment: Introducing AsyncFuse

After another painful debugging session trying to figure out why a loading spinner wouldn't disappear, I had enough. The problem wasn't the microfrontends themselves; it was the *seams* between them. We needed something to fuse them together gracefully. And that’s the idea behind **AsyncFuse**.

What is AsyncFuse?

AsyncFuse isn't another framework. It's a tiny, framework-agnostic library (~2kb) that provides a declarative, component-based way to load, manage, and communicate with your async microfrontends. It treats async loading, error handling, and communication as first-class citizens, not afterthoughts.

Think of it as a smart container for your MFEs. You tell it *what* to load, and it handles the *how*—and all the messy states in between.

From Chaos to Clarity: A Real Example

Here’s what our host application code looked like before. This is for a single MFE. Imagine this x10.

Before: The Manual JavaScript Soup

// in host-app.js
const container = document.getElementById('mfe-container');

// 1. Manually set loading state
container.innerHTML = '<div class="spinner"></div>';

async function loadMFE() {
  try {
    // 2. Dynamic import
    const mfe = await import('https://mfe-server.com/recommendations.js');
    // 3. Manually clear loading state
    container.innerHTML = '';
    // 4. Mount the MFE
    mfe.mount(container, { userId: '123' });
  } catch (err) {
    // 5. Manually set error state
    container.innerHTML = '<div class="error-fallback">Could not load.</div>';
    console.error('MFE load failed:', err);
  }
}

loadMFE();

It works, but it's imperative and fragile. Now, look at the same logic with AsyncFuse.

After: Declarative and Robust with AsyncFuse

<!-- in host-app.html -->
<async-fuse
  src="https://mfe-server.com/recommendations.js"
  props-user-id="'123'"
>
  <!-- Slot for loading state -->
  <div slot="loading">
    <div class="skeleton-loader"></div>
  </div>

  <!-- Slot for error state -->
  <div slot="error">
    <p>Oops! Recommendations are currently unavailable.</p>
  </div>
</async-fuse>

That's it. All the boilerplate is gone. We just declare the MFE source and provide templates for the loading and error states. AsyncFuse handles the entire lifecycle.

Key Features That Make the Difference

  • Zero-Config State Management: It automatically handles `loading` and `error` states via HTML slots. No more manual DOM manipulation for spinners and fallbacks.
  • Built-in Timeout: You can add a `timeout="5000"` attribute. If the MFE doesn't load in 5 seconds, it automatically shows the error state. A lifesaver for flaky networks.
  • Unified Event Bus: AsyncFuse provides a simple, scoped event bus. An MFE can emit an event like `AsyncFuse.publish('add-to-cart', { item })` and any other MFE (or the host) can subscribe with `AsyncFuse.subscribe('add-to-cart', callback)`. It intelligently queues events, so you don't have to worry about publish/subscribe timing issues.
  • Declarative Props: Passing data to the MFE is as simple as adding `props-` attributes to the tag. AsyncFuse gathers them and passes them as a single object to your MFE's `mount` function.

Your Key Takeaways

Switching our mindset from imperative loading to a declarative, component-based approach was the game-changer. By focusing on a tool that solves the *orchestration* problem, we simplified our entire architecture.

  • The problem is the seams: Most of the complexity in async MFEs lies in managing the states and communication *between* them.
  • Declarative is better than imperative: Defining *what* you want in HTML is less error-prone and easier to reason about than writing JavaScript to manage every step of the process.
  • Simplicity is a feature: Don't reach for the most powerful tool (like Module Federation) if a simpler one (like AsyncFuse) solves your core problem more elegantly.

Is AsyncFuse Right for You?

AsyncFuse shines in environments where multiple teams are building framework-agnostic microfrontends that need to be loaded asynchronously into a host application. If your primary struggles are with loading/error states and cross-MFE communication, it's a perfect fit.

When might it *not* be the best choice? If you have a very complex dependency graph where sharing library instances is critical for performance, the heavier machinery of Webpack Module Federation might still be necessary. But for the 80% of use cases, a simpler orchestration layer is all you need.

In 2025, we shouldn't be re-solving async loading and error handling. We have better things to build. By abstracting away the boilerplate, a simple tool like AsyncFuse let us get back to what matters: shipping great features for our users.

You May Also Like