---
title: "Debugging frontend crash and handling circular dependency"
description: "Debugging frontend crash and handling circular dependency"
canonical_url: "https://www.bigbinary.com/blog/debugging-frontend-crash-and-handling-circular-dependency"
markdown_url: "https://www.bigbinary.com/blog/debugging-frontend-crash-and-handling-circular-dependency.md"
---

# Debugging frontend crash and handling circular dependency

Debugging frontend crash and handling circular dependency

- Author: Joseph Mathew
- Published: June 10, 2026
- Categories: JavaScript, ReactJS

Recently, we had a crash in our [NeetoCRM](https://www.neeto.com/neetocrm)
application.

![crash screenshot](https://www.bigbinary.com/blog/images/images_used_in_blog/2026/investigating-frontend-crash/crash-screenshot.png)

As we can see in the screenshot, it looks like the error happened in
`neeto-widget-replay.js` file.

At [Neeto](https://neeto.com) we have built an internal tool called NeetoReplay
which captures users' activities in the browser. This helps us in debugging when
users contact us for support or in investigating bugs. This is built on top of
[rrweb](https://github.com/rrweb-io/rrweb). _Just want to add that admins of the
workspace can completely opt out of NeetoReplay._

We had not changed anything in NeetoReplay for a while, so the error happening
in NeetoReplay was perplexing. Upon investigation, I found that the console
indeed pointed to the `neeto-widget-replay.js` file. But I also knew that the
filename only tells us which function called `console.error`, not where the
error originated.

The replay widget wraps `console.log`, `console.warn`, and `console.error` so it
can capture console output for session replay. Once the page loads, every
console message passes through the widget's wrapper, causing DevTools to
associate those messages with the widget file. As a result, when the NeetoCRM
app throws an error during startup, the console makes it look like the error
came from the replay widget even though the actual exception was thrown inside
the NeetoCRM bundle.

The way rrweb's console plugin works, it replaces `console.log`, `console.warn`,
and `console.error` so console output can be captured for session replay. I
checked whether rrweb provides a way to preserve the original caller
information, but it doesn't. Removing the wrapper would also mean losing console
capture from replays.

This isn't specific to rrweb either. Honeybadger, Sentry, PostHog, and React
DevTools use similar wrapping and have the same behavior. React DevTools has an
identical issue documented in
[facebook/react#22257](https://github.com/facebook/react/issues/22257).

The only known mitigation is Chrome's `x_google_ignoreList` source map feature,
which allows DevTools to hide library frames and show the host application frame
instead. However, it only works when source maps are available. Since we don't
ship source maps with the production minified bundle, this isn't available in
production. As a result, the console attribution is an unavoidable side effect
of session replay tooling, but the actual error information remains intact.

The video mentioned below is the one I created for the internal Neeto folks. The
video is being published **as-is** without any modifications.

<iframe
  width="560"
  height="315"
  src="https://www.youtube.com/embed/cBZvoN5HQKo?si=zy0cpU7YIF9yG4gv"
  title="Debugging frontend crash and handling circular dependency"
  frameborder="0"
  allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
  referrerpolicy="strict-origin-when-cross-origin"
  allowfullscreen
></iframe>

## Back to the error

The real error is:

```
TypeError: I is not a function at chunk-CL4SWXVJ.digested.js
```

`I` is the minified name for `createColumn`, a helper defined in
`commons/utils.jsx`.

The failure occurs because code in `commons/constants.js` calls
`createColumn(...)` before the function has been initialized. At runtime,
`createColumn` is still `undefined`, causing the call to fail and preventing the
React application from starting. The subsequent `Failed to load component App`
message is simply the mount code reporting that application initialization
failed.

Tracing this back further shows that `commons/constants.js` and
`commons/utils.jsx` have a circular dependency. `constants.js` imports
`createColumn` from `utils.jsx` while `utils.jsx` imports values from
`constants.js`.

In development, Vite serves modules as native ES modules and the browser
evaluates them depth-first. When `constants.js` imports `createColumn` from
`utils.jsx`, the browser evaluates `utils.jsx` first, which initializes
`createColumn` before control returns to `constants.js`. By the time
`constants.js` calls `createColumn`, the function is already defined.

In production, builds are generated using esbuild, which bundles everything into
a single file and has to choose a linear execution order. This can introduce
subtle differences in behavior, particularly around module evaluation order and
circular dependencies. In the generated bundle, code from `constants.js` ends up
running before `createColumn` is initialized, which causes the crash.

The reason this surfaced now is that a recent change introduced the following
import in the Deals, Leads, and Contacts `Show/index.jsx` files:

```
import { buildHeaderMoreMenu } from "components/commons/utils";
```

Those files did not previously import from `commons/utils`. The circular
dependency already existed, but this additional import changed esbuild's
bundling order and exposed the issue. The change did not introduce the circular
dependency itself; it only made the existing problem surface in production.

## The fix

The fix is to break the circular dependency by moving `createColumn` into a
small standalone module that has no dependency on `commons/constants.js`. Once
the circular import is removed, esbuild can no longer generate an execution
order where `createColumn` is referenced before initialization.

## How to replicate this behavior in the development environment

To help catch these issues earlier, we provide an esbuild-based development
server that mirrors production bundling behavior while still supporting
automatic rebuilds during development.

Start the Rails server with the `ESBUILD_DEVSERVER` flag enabled.

```
ESBUILD_DEVSERVER=true bundle exec rails server
```

In a separate terminal, start esbuild in watch mode.

```
yarn build --watch
```

With `ESBUILD_DEVSERVER=true`, Rails serves the assets generated by esbuild
instead of the assets served by Vite. Running `yarn build --watch` ensures the
bundles are rebuilt automatically whenever files change, allowing you to test
the application using the same bundling behavior as production.

## Why Honeybadger didn't catch this error

If JavaScript execution fails before the React tree mounts, Honeybadger never
receives the error. This includes chunk load failures, syntax errors in the
entry chunk, runtime errors during module evaluation, and bundling issues caused
by incorrect module ordering.

The error was thrown while evaluating the `App` chunk and was visible in the
browser console for every affected user. However, no Honeybadger issue was
created.

The initialization flow looked like this:

```
application.js
└─ mount({ App: () => import("src/App") })
     └─ dynamic import("src/App")
          └─ <App>
               └─ <AppContainer>
                    └─ <HoneybadgerErrorBoundary>
```

`Honeybadger.configure()` is called inside `HoneybadgerErrorBoundary`. Since
that component is rendered as a descendant of `<App>`, Honeybadger is
initialized only after the `App` chunk has been successfully loaded and
rendered.

When a startup failure occurs, `import("src/App")` rejects, `mount.js` catches
the error and logs it using `console.error(...)`, and the execution stops before
`<App>` is mounted. Because the error has already been handled, `window.onerror`
is never triggered. Since `<HoneybadgerErrorBoundary>` never renders,
`Honeybadger.configure()` is never called, which means no API key is configured,
no global handlers are registered, and no error is reported.

As a result, startup failures that completely prevent the application from
loading are invisible to Honeybadger.

Honeybadger should be initialized before loading the `App` chunk so that
failures during application bootstrap, chunk loading, and module evaluation are
captured and reported.

## The Honeybadger fix

Honeybadger is now configured in `application.js`, before `mount()` runs:

```
application.js
├─ Honeybadger.configure({ enableUncaught: true, ... })   // configured before mount
└─ mount({ App: () => import("src/App") })
     ├─ dynamic import("src/App") succeeds
     │    └─ <App>
     │         └─ <AppContainer>
     │              └─ <HoneybadgerErrorBoundary>   // uses pre-configured client
     │
     └─ dynamic import("src/App") fails
          └─ Honeybadger.notify(error, { name: "AppMountError" })
```

With `enableUncaught: true`, `window.onerror` is armed before any dynamic import
runs, so failures during module evaluation are captured. And if the `App` chunk
itself fails to load, `mount()` reports it explicitly via
`Honeybadger.notify(...)` instead of only logging to the console.

## Links

- [Human page](https://www.bigbinary.com/blog/debugging-frontend-crash-and-handling-circular-dependency)
