Skip to content

A Microfrontend May Fail

Microfrontends introduce new runtime boundaries. With dynamic integration in particular, a remote can become unavailable independently of the shell, fail to activate, or break while it is being used.

Martin Fowler and James Lewis describe “design for failure” as one of the defining characteristics of microservice architectures: applications have to account for individual services becoming unavailable and respond as gracefully as possible.1 The same expectation can be applied to microfrontends:

The failure of a remote should affect its product area, not the entire application.

This is not an argument against dynamic microfrontends. It is a consequence of distributed delivery. Additional autonomy creates additional failure states. An architecture must therefore be designed for more than the successful state.

A distributed architecture that only works while every part is available is merely a distributed happy path.

The decisive question is not whether a remote can fail, but how far that failure reaches.

With dynamic microfrontends, the shell may remain available while an individual remote artifact cannot be loaded. A manifest may be missing, a server may fail to respond, or an artifact may have been delivered incompletely. There may also be failures during activation or inside a remote that has already started.

These states are not automatically signs of poor architecture. They arise because the parts are operated independently. That independence has practical value only if the platform can also cope with one of those parts failing.

A microfrontend is only truly independent when its failure also has a bounded reach.

A Remote May Fail — the Product Must Not

Section titled “A Remote May Fail — the Product Must Not”

The title is deliberately provocative. It does not imply that a remote is unimportant or that its failure is acceptable. Business-critical capabilities must not become arbitrarily unreliable merely because they live in a separate remote.

The affected use case still does not have to remain fully functional. If the invoicing remote fails, invoice processing may be unavailable. Resilience does not require the shell to provide a replacement implementation.

The meaningful expectation is smaller — and at the same time more demanding architecturally:

Not a meaningful expectation:
└── The failed capability still works in full
Meaningful expectation:
├── The failure remains inside the affected area
├── The rest of the product remains usable
├── The state is explained clearly
└── Retry or return remains possible

The failure is anticipated and technically bounded. Navigation and other product areas continue to work. The affected area presents an understandable state; retry, return, and targeted diagnosis remain possible.

Resilience does not mean that the affected use case continues to work despite the failure. It means that the entire product does not fail as well.

Not every remote failure occurs in the same phase. For the integration, it is useful to distinguish at least three classes of failure.

The first failure occurs before activation. The shell tries to load a manifest, a JavaScript artifact, or a dynamic entry point, but does not receive a usable result within a reasonable period.

Possible causes include:

  • an unavailable remote manifest,
  • an unreachable JavaScript artifact,
  • an outage of the CDN or server,
  • a network error,
  • a timeout,
  • an artifact that was delivered incompletely or incorrectly.

The shell has to recognize this state within a reasonable period. A permanently visible global loading indicator is not a fallback. The affected area needs a defined error state; the rest of the application must not wait indefinitely.

In the second class of failure, the artifact has been loaded, but bootstrap, mounting, or initialization fails.

Initialization may throw an exception, a platform adapter may be missing, mounting may fail, or an integration contract may not be satisfied. An initialization that never completes successfully belongs to this category as well.

For the reach of the failure, what matters is that the shell treats activation as failed and moves the mounting area into a defined error state. A loaded entry point is not yet a running remote.

The third class occurs after successful activation. The remote is visible and being used, but later throws an unhandled error.

That may be caused by a rendering or template error, an uncaught exception, faulty asynchronous processing, an unexpected state, or an unhandled error inside a use case.

The running product area also needs an effective boundary so that a later failure does not destroy the entire visible application tree.

Deployment Boundaries Are Not Yet Error Boundaries

Section titled “Deployment Boundaries Are Not Yet Error Boundaries”

A separate repository, its own build artifact, and independent deployment create organizational and technical boundaries. They do not automatically create an isolated runtime.

Separate deployments do not automatically create separate failure domains.

Remotes running in the same browser document and JavaScript context share essential resources: the main thread, browser objects, and the visible document tree.

A remote can affect the rest of the product through:

  • uncaught failures outside an effective integration boundary,
  • global event handlers,
  • global CSS rules,
  • changes to shared browser objects,
  • blocking synchronous code,
  • uncontrolled global side effects.

Those effects require different countermeasures. A rendering error is not the same as blocking code; global CSS rules are not constrained by an error handler.

The word microfrontend does not create an error boundary.

A global error handler can record failures and therefore be operationally valuable. It does not automatically isolate the affected area. A framework-specific error boundary may catch certain failures inside a component tree, but it does not necessarily cover asynchronous failures, global side effects, or failures outside its scope.

The effective error boundary has to match the actual integration model.

Failures Must Be Bounded at the Mounting Point

Section titled “Failures Must Be Bounded at the Mounting Point”

The natural place for a deliberate error boundary is the integration point between platform and remote. At that point, the shell knows the technical lifecycle of the integrated part without having to take over its domain logic.

One possible model looks like this:

Shell
└── Remote Boundary
├── Loading
├── Mounted Remote
├── Activation Error
└── Runtime Fallback

The shell or an integration adapter controls the technical transitions:

  1. Loading begins.
  2. A timeout or loading failure is detected.
  3. Mounting is attempted.
  4. Failures during activation are caught.
  5. A defined fallback is rendered after a failure.
  6. A retry may be offered.
  7. Required cleanup is performed before replacement or retry.

This responsibility is platform logic. It does not require domain knowledge about invoices, tasks, orders, or plans.

The platform has to know that a remote is unavailable. It does not have to know which domain operation failed inside it.

That is the important separation: the shell controls the technical lifecycle. The remote remains responsible for its domain states and failures as long as it is still capable of handling them.

The mounting point is the platform’s outer error boundary, not the remote’s only form of error handling. The remote remains responsible for expected domain failures and its internal states. The integration boundary nevertheless creates a place where loading, activation, outer runtime failure, fallback, and recovery can be coordinated deliberately.

A comparison shows a separately delivered remote without a controlled error boundary and a deliberate integration boundary with failure detection, a local fallback, and an application that remains usable.

The shell can contain the failure of a remote. It is also part of the failure architecture itself.

Remote fails
└── one product area is unavailable
Host fails
└── the composed application is unavailable

The host is the shared entry point of the composed application. It provides navigation, activates remotes, exposes platform contracts, and renders fallbacks. If it fails completely, it can no longer perform any of those tasks.

The shell can contain the failure of a remote. It cannot contain its own complete failure from within the same application.

That is a real boundary of the model: the component that contains failures in other parts must itself remain available.

This gives the demand for a domain-thin shell another meaning: more volatile domain logic also increases its failure impact.

The shell therefore needs:

  • low domain volatility,
  • stable platform contracts,
  • robust delivery,
  • few unnecessary runtime dependencies,
  • strong observability,
  • controlled change,
  • clear technical ownership.

The shell does not have to be small at any cost, but it has to be reliable as the shared technical frame.

The graphic compares a locally contained remote failure with a complete host failure. When the remote fails, navigation, other remotes, and a local fallback remain available; when the host fails, the entire composed application is affected.

A Remote Artifact Is Not Yet a Standalone Application

Section titled “A Remote Artifact Is Not Yet a Standalone Application”

With dynamic microfrontends, a remote can theoretically remain reachable even when the actual host is unavailable. That statement is only correct under additional conditions.

remoteEntry.js
standalone navigable product

A remote artifact is a technical entry point. It does not automatically include navigation, an authentication context, or all required platform adapters.

For direct access, the remote needs a standalone entry point or a mini-host:

Remote
├── integration into the actual host
└── standalone entry point or mini-host

A mini-host can provide routing, theme, locale, authentication context, platform adapters, and API connectivity.

A dynamic remote can remain reachable independently of the actual host when it is also operated through a standalone entry point. Merely being able to reach its remote artifact is not enough.

This does not create a universal requirement. A mini-host may exist exclusively for development and testing and does not have to be part of the production failure strategy.

Whether a remote should also operate as a product entry point without the actual host is a deliberate product and operational decision — not an automatic property of dynamic integration.

Static and Dynamic Integration Fail Differently

Section titled “Static and Dynamic Integration Fail Differently”

Static integration avoids the additional remote request at runtime. The integrated state is built together and delivered as one application. A manifest or loading failure of a separately operated remote artifact therefore does not occur.

Statically integrated areas can still fail at runtime. Rendering failures, exceptions, and global side effects still need to be bounded.

At the same time, a faulty shared build or release can potentially affect the entire application. The failure then does not lie in runtime activation of a separate remote, but in the jointly delivered state.

With dynamic integration, an individual remote can instead become unavailable independently. Network, manifest, and activation failures appear only at runtime. The shell can detect those states, degrade locally, and load the newly published remote again after a correction.

A smaller deployment unit can contain delivery failures more effectively. It is neither a prerequisite nor a guarantee of a smaller runtime failure domain. Statically integrated areas can also be isolated through deliberately placed error boundaries.

Dynamic integration enables smaller failure domains. It does not create them automatically.

Both integration models therefore need error boundaries. They differ primarily in when and how failures become visible.

Graceful Degradation Preserves the Product

Section titled “Graceful Degradation Preserves the Product”

Graceful degradation means that the product falls back to a constrained state in a controlled way. The failed capability is not necessarily replaced.

A meaningful degraded state can:

  • clearly mark the affected product area,
  • keep navigation and other remotes available,
  • offer another loading attempt,
  • show an understandable error message,
  • provide a link to a status or support page,
  • mark existing data as potentially outdated,
  • allow a safe return.

It must not pretend that a write operation succeeded. It must not present stale data as current. It must not improvise domain decisions. Nor should it buffer unsecured operations locally without a sound domain and technical concept.

Graceful degradation preserves the product, not necessarily the failed capability.

A degraded product remains understandable and usable without pretending to provide a second implementation of the domain capability.

Fallbacks Do Not Replace a Domain Capability

Section titled “Fallbacks Do Not Replace a Domain Capability”

A fallback is an alternative representation of the failure state. It is not automatically an alternative implementation of the failed product area.

Invoicing remote unavailable
├── Navigation remains usable
├── Other product areas remain accessible
├── Failure state is displayed
└── Retry remains possible

This would not be sensible:

Invoicing remote unavailable
└── Shell spontaneously reconstructs invoicing

A fallback does not replace the remote. It prevents the remote’s failure from taking down the entire application.

The fallback must also fit the affected use case. For an optional analytics area, a local message with retry may be sufficient. For a business-critical process, additional status information, a support path, or an alternative access route may be necessary.

A universal error card is therefore, at most, a technical foundation. It does not replace the domain-specific design of the failure state.

The failure state is part of the product experience. A message such as “Something went wrong” barely fulfills that responsibility. It neither identifies the affected area nor explains what remains possible.

Depending on the concrete case, a good failure state should answer:

  • Which area is currently unavailable?
  • Are other areas still usable?
  • Can the operation be retried?
  • Has previously entered data been preserved?
  • Could the action still have been completed?
  • Is there an alternative path?
  • When should support be contacted?

Not every question is relevant for every failure. What matters is that the presentation is designed from the perspective of the affected person rather than merely reflecting the loader’s technical state.

Resilience does not appear in the perfect state. It appears in how understandable the product remains in the bad state.

A deliberately degraded state makes the failure visible without blocking the entire application.

The graphic compares a normal product area with an active remote against a controlled degraded state with an understandable message, retry or return, and navigation that remains usable.

Write Operations Need Honest Failure States

Section titled “Write Operations Need Honest Failure States”

For read use cases, trying again is often relatively unproblematic. If loading an overview fails, the application can display the failure and reload later.

With write operations, the state is more difficult. After a timeout or broken connection, it may be unclear:

  • whether the request reached the backend,
  • whether the operation was already executed,
  • whether only the response was lost,
  • whether retrying the action would execute it twice.

The fallback must therefore not casually claim that the action failed when its actual state is unknown.

An honest degraded state has to distinguish — where this can be determined reliably — between “not executed”, “executed”, and “status unknown”. If that distinction cannot be made, exactly that uncertainty has to be communicated clearly.

Idempotent operations can make retries safer. The UI must still not turn technical uncertainty into domain certainty.

Automatic retries therefore require particular caution for write operations. Silently repeating a read request is different from submitting an order, booking, or approval several times.

A locally contained failure improves the user experience. It becomes operationally useful only when it remains observable as well.

The platform should be able to record technical information such as:

  • which remote was affected,
  • in which lifecycle phase the failure occurred,
  • whether loading, activation, or runtime was affected,
  • which version or artifact was active,
  • whether a retry succeeded,
  • how frequently the failure occurs.

Personal data, tokens, and domain data should not be logged unnecessarily. Good diagnosis does not come from collecting as much data as possible, but from targeted technical context.

The user message and the technical diagnosis serve different goals:

User
└── receives an understandable product state
Operations
└── receives a technical diagnosis

A fallback without observability protects the UI, but not operations.

Resilience must not make failures invisible. It has to contain their reach while keeping their causes discoverable.

Another loading attempt can be useful for temporary network failures, briefly unavailable artifacts, or a failed dynamic import.

Not every failure improves through repetition. An initialization that fails deterministically is likely to fail on the tenth attempt just as it did on the first. Repeated mounting attempts without cleanup can make the state even worse.

The following should be avoided in particular:

  • endless loading states,
  • aggressive automatic retries,
  • repeated mounting attempts without cleanup,
  • repeated write operations without safe semantics.

A manual retry is often easier to understand than an invisible, unlimited repetition. It gives the affected person control and can be connected to a clear state transition.

Automatic recovery can be useful when the failure class, boundary, and semantics are known. Its behavior has to remain understandable from the domain perspective.

Remotes in the same document share much of their runtime. Stronger technical isolation is possible, for example, through separate documents or iframes.

That can provide clearer boundaries for certain failure effects. A separate document has its own JavaScript context and a more explicit technical boundary from the surrounding application.

This isolation also introduces higher integration costs, including navigation, styling, accessibility, communication, routing, authentication, and a consistent product experience.

The stronger the technical isolation, the clearer some failure boundaries can become. At the same time, integration costs rise.

The necessary level of isolation depends on criticality, accepted failure effects, and integration cost.

The Failure Stays with the Affected Product Area

Section titled “The Failure Stays with the Affected Product Area”

A sustainable microfrontend has more than a deployment boundary. It also has a deliberately established error boundary.

If a remote cannot be loaded, activated, or kept running, the failure remains bounded to the affected product area. The platform preserves navigation and the remaining capabilities. It shows an understandable degraded state, enables a safe return or controlled retry, and keeps the failure observable for operations.

A microfrontend is only truly independent when its failure also has a bounded reach.

Separate deployments do not automatically create separate failure domains. The error boundary emerges only through controlled integration at the mounting point, suitable fallbacks, and product design that takes the bad state seriously.

Graceful degradation preserves the product, not necessarily the failed capability. It replaces neither the remote’s domain responsibility nor the obligation to fix the failure.

The shell can contain the failure of a remote. It remains a shared failure domain itself. That is exactly why it should be stable, observable, and as low in volatile domain logic as possible.

The failure of a remote may cost one capability, but it must not cost the entire product.

  1. Martin Fowler and James Lewis, “Microservices” — Design for Failure; additionally Martin Fowler, “Microservice Trade-Offs”.