# Isolate module failures

One of the key characteristics of a modular implementation such as iframes is the ability to isolate failures within individual iframe, preventing them from breaking the entire application.

However, this characteristic is not inherent to a standard Squide implementation as all the modules share the same browsing context (e.g. the same Document object, the same Window object, and the same DOM). A failure in one module can potentially breaks the entire application.

That said, a Squide application can achieve near-iframe-level failure isolation by leveraging React Router's Outlet along with the errorElement property of a React Router's routes. This approach allows individual routes (and their associated modules) to handle errors gracefully, preventing them from cascading and affecting the rest of the app.

# Create an error boundary

First, define a React Router's error boundary to catch module errors. For this example we'll name it ModuleErrorBoundary:

host/src/ModuleErrorBoundary.tsx
export function ModuleErrorBoundary() {
    return (
        <div>An error occured while rendering a page from a module!</div>
    )
}

# Register the error boundary

Then, update the host application registerHost function to declare the ModuleErrorBoundary component below the RootLayout component but above the routes of the modules. By doing so, if a module encounters an unhandled error, the error boundary will only replace the section rendered by the Outlet component within the root layout rather than the entire page.

A React Router's error boundary is declared with the errorElement of a route:

host/src/register.tsx
import { PublicRoutes, ProtectedRoutes, type ModuleRegisterFunction, type FireflyRuntime } from "@squide/firefly";
import { RootLayout } from "./RootLayout.tsx";
import { ModuleErrorBoundary } from "./ModuleErrorBoundary.tsx";

export const registerHost: ModuleRegisterFunction<FireflyRuntime> = runtime => {
    runtime.registerRoute({
        element: <RootLayout />,
        children: [
            {
                // Error boundary for modules.
                errorElement: <ModuleErrorBoundary />,
                children: [
                    PublicRoutes,
                    ProtectedRoutes
                ]
            }
        ]
    }, {
        hoist: true
    });
};

By implementing this mechanism, the level of failure isolation achieved is comparable to that of an iframes implementation.

# Hoisted pages

If your application is hoisting pages, it's important to note that they will be rendered outside of the host application's ModuleErrorBoundary component. To prevent breaking the entire application when an hoisted page encounters unhandled errors, it is highly recommended to declare a React Router's error boundary for each hoisted page as well, again using errorElement:

local-module/src/register.tsx
import { type ModuleRegisterFunction, type FireflyRuntime } from "@squide/firefly";
import { Page } from "./Page.tsx";
import { RemoteErrorBoundary } from "./RemoteErrorBoundary.tsx";

export const register: ModuleRegisterFunction<FireflyRuntime> = runtime => {
    runtime.registerRoute({
        path: "remote/page", 
        element: <Page />,
        errorElement: <RemoteErrorBoundary />
    }, {
        hoist: true
    });
};

# Try it 🚀

Start the application in a development environment using the dev script. Update any of your application routes that is rendered under the newly created error boundary (e.g. that is not hoisted) and throw an Error. The error should be handled by the ModuleErrorBoundary component instead of breaking the whole application.

# Troubleshoot issues

If you are experiencing issues with this guide:

  • Open the DevTools console. You'll find a log entry for each registration that occurs and error messages if something went wrong.
  • Refer to a working example on GitHub.
  • Refer to the troubleshooting page.