Trending
Global Youth Competition on Influenza and COVID-19 Prevention and Control is trending now Nigeria: Donor Cuts, Govt Inaction Push HIV Patients Towards Death, Uncertainty in Rivers… is trending now Grant Competition for Non-Governmental HIV Service Organizations (Ukraine) is trending now Ebola Outbreak in DRC Hits Livelihoods and Healthcare Access is trending now Partial lunar eclipse live: Latest news, viewing tips and live updates from Aug. 27-28 ec… is trending now Transfer latest: Konsa, Jones & Baleba updates plus reporter Q&A is trending now Ballon d’Or 2026: Three players ahead of Harry Kane to win award – Didi Hamann is trending now 'A leap in quality' - Diego Simeone praises substitutes as Atletico Madrid open La Liga c… is trending now Mikel Arteta Sends Warning Ahead of Arsenal's Premier League Title Defense is trending now Open Call: Socially Engaged Art Support Grant FY2027 is trending now 'We were dating same girl' - Blaqbonez finally reveals cause of feud with Odumodublvck [V… is trending now FULL LIST: Burna Boy, Tems make 2026 MTV VMA nominees list is trending now Global Youth Competition on Influenza and COVID-19 Prevention and Control is trending now Nigeria: Donor Cuts, Govt Inaction Push HIV Patients Towards Death, Uncertainty in Rivers… is trending now Grant Competition for Non-Governmental HIV Service Organizations (Ukraine) is trending now Ebola Outbreak in DRC Hits Livelihoods and Healthcare Access is trending now Partial lunar eclipse live: Latest news, viewing tips and live updates from Aug. 27-28 ec… is trending now Transfer latest: Konsa, Jones & Baleba updates plus reporter Q&A is trending now Ballon d’Or 2026: Three players ahead of Harry Kane to win award – Didi Hamann is trending now 'A leap in quality' - Diego Simeone praises substitutes as Atletico Madrid open La Liga c… is trending now Mikel Arteta Sends Warning Ahead of Arsenal's Premier League Title Defense is trending now Open Call: Socially Engaged Art Support Grant FY2027 is trending now 'We were dating same girl' - Blaqbonez finally reveals cause of feud with Odumodublvck [V… is trending now FULL LIST: Burna Boy, Tems make 2026 MTV VMA nominees list is trending now
Reno

Debugging frontend crash and handling circular dependency

Debugging frontend crash and handling circular dependency

Recently, we had a crash in our NeetoCRMapplication.As we can see in the screenshot, it looks like the error happened inneeto-widget-replay.js file.At Neeto we have built an internal tool called NeetoReplaywhich captures users' activities in the browser. This helps us in debugging whenusers contact us for support or in investigating bugs. This is built on top ofrrweb. Just want to add that admins of theworkspace can completely opt out of NeetoReplay.We had not changed anything in NeetoReplay for a while, so the error happeningin NeetoReplay was perplexing. Upon investigation, I found that the consoleindeed pointed to the neeto-widget-replay.js file. But I also knew that thefilename only tells us which function called console.error, not where theerror originated.The replay widget wraps console.log, console.warn, and console.error so itcan capture console output for session replay. Once the page loads, everyconsole message passes through the widget's wrapper, causing DevTools toassociate those messages with the widget file. As a result, when the NeetoCRMapp throws an error during startup, the console makes it look like the errorcame from the replay widget even though the actual exception was thrown insidethe 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. Ichecked whether rrweb provides a way to preserve the original callerinformation, but it doesn't. Removing the wrapper would also mean losing consolecapture from replays.This isn't specific to rrweb either. Honeybadger, Sentry, PostHog, and ReactDevTools use similar wrapping and have the same behavior. React DevTools has anidentical issue documented infacebook/react#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 frameinstead. However, it only works when source maps are available. Since we don'tship source maps with the production minified bundle, this isn't available inproduction. As a result, the console attribution is an unavoidable side effectof session replay tooling, but the actual error information remains intact.The video mentioned below is the one I created for the internal Neeto folks. Thevideo is being published as-is without any modifications. import("src/App") }) dynamic import("src/App") Honeybadger.configure() is called inside HoneybadgerErrorBoundary. Sincethat component is rendered as a descendant of , Honeybadger isinitialized only after the App chunk has been successfully loaded andrendered.When a startup failure occurs, import("src/App") rejects, mount.js catchesthe error and logs it using console.error(...), and the execution stops before is mounted. Because the error has already been handled, window.onerroris never triggered. Since 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 fromloading are invisible to Honeybadger.Honeybadger should be initialized before loading the App chunk so thatfailures during application bootstrap, chunk loading, and module evaluation arecaptured and reported.The Honeybadger fixHoneybadger 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 // uses pre-configured client dynamic import("src/App") fails Honeybadger.notify(error, { name: "AppMountError" })With enableUncaught: true, window.onerror is armed before any dynamic importruns, so failures during module evaluation are captured. And if the App chunkitself fails to load, mount() reports it explicitly viaHoneybadger.notify(...) instead of only logging to the console.

Recently, we had a crash in our NeetoCRMapplication.

crash screenshot

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

At Neeto we have built an internal tool called NeetoReplaywhich captures users' activities in the browser. This helps us in debugging whenusers contact us for support or in investigating bugs. This is built on top ofrrweb. Just want to add that admins of theworkspace can completely opt out of NeetoReplay.

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

The replay widget wraps console.log, console.warn, and console.error so itcan capture console output for session replay. Once the page loads, everyconsole message passes through the widget's wrapper, causing DevTools toassociate those messages with the widget file. As a result, when the NeetoCRMapp throws an error during startup, the console makes it look like the errorcame from the replay widget even though the actual exception was thrown insidethe 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. Ichecked whether rrweb provides a way to preserve the original callerinformation, but it doesn't. Removing the wrapper would also mean losing consolecapture from replays.

This isn't specific to rrweb either. Honeybadger, Sentry, PostHog, and ReactDevTools use similar wrapping and have the same behavior. React DevTools has anidentical issue documented infacebook/react#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 frameinstead. However, it only works when source maps are available. Since we don'tship source maps with the production minified bundle, this isn't available inproduction. As a result, the console attribution is an unavoidable side effectof session replay tooling, but the actual error information remains intact.

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

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 incommons/utils.jsx.

The failure occurs because code in commons/constants.js callscreateColumn(...) before the function has been initialized. At runtime,createColumn is still undefined, causing the call to fail and preventing theReact application from starting. The subsequent Failed to load component Appmessage is simply the mount code reporting that application initializationfailed.

Tracing this back further shows that commons/constants.js andcommons/utils.jsx have a circular dependency. constants.js importscreateColumn from utils.jsx while utils.jsx imports values fromconstants.js.

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

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

The reason this surfaced now is that a recent change introduced the followingimport 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 circulardependency already existed, but this additional import changed esbuild'sbundling order and exposed the issue. The change did not introduce the circulardependency itself; it only made the existing problem surface in production.

The fix

The fix is to break the circular dependency by moving createColumn into asmall standalone module that has no dependency on commons/constants.js. Oncethe circular import is removed, esbuild can no longer generate an executionorder 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 developmentserver that mirrors production bundling behavior while still supportingautomatic 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 esbuildinstead of the assets served by Vite. Running yarn build --watch ensures thebundles are rebuilt automatically whenever files change, allowing you to testthe 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 neverreceives the error. This includes chunk load failures, syntax errors in theentry chunk, runtime errors during module evaluation, and bundling issues causedby incorrect module ordering.

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

The initialization flow looked like this:

application.js mount({ App: () => import("src/App") })      dynamic import("src/App")                                                

Honeybadger.configure() is called inside HoneybadgerErrorBoundary. Sincethat component is rendered as a descendant of , Honeybadger isinitialized only after the App chunk has been successfully loaded andrendered.

When a startup failure occurs, import("src/App") rejects, mount.js catchesthe error and logs it using console.error(...), and the execution stops before is mounted. Because the error has already been handled, window.onerroris never triggered. Since 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 fromloading are invisible to Honeybadger.

Honeybadger should be initialized before loading the App chunk so thatfailures during application bootstrap, chunk loading, and module evaluation arecaptured 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                                                // uses pre-configured client           dynamic import("src/App") fails           Honeybadger.notify(error, { name: "AppMountError" })

With enableUncaught: true, window.onerror is armed before any dynamic importruns, so failures during module evaluation are captured. And if the App chunkitself fails to load, mount() reports it explicitly viaHoneybadger.notify(...) instead of only logging to the console.

View original source →

Related