Engineering Journal
Schema Editor
Schema Editor

Postmortem: The 2500-Line File Was Fine Until We Split It

2026-06-04

TLDR

A single 2500-line index.html with all JS inline worked. Splitting it into modules surfaced three classes of bugs: arrow functions with broken $(this), initialization order errors from circular-looking imports, and implicit state dependencies that were invisible inside a shared closure. The bugs were not created by the split. They were revealed by it.


The Assumption That Seemed Reasonable

A large single-file codebase is a starting point. You iterate fast, everything is in one place, there are no import errors, no build steps. When the codebase is ready for production, you split it into proper modules. Splitting is a cleanup task, not a risky refactor.

This assumption is correct about the first part: monolith development is fast. It is wrong about the second: splitting is not cleanup. It is a refactor that must handle every bug the monolith hid.

When It Failed

The first failure was the trace wire button. The click handler used an arrow function with $(this):

$('#traceWireBtn').on('click', () => {
    const mode = $(this).data('mode'); // arrow function: 'this' is not the button
    self.setMode(mode);
});

In the monolith, $(this) in an arrow function refers to the outer this, which was the window-level module object. The data('mode') attribute happened to exist on the module object from a previous assignment. The handler worked.

After the split, the module object no longer had data('mode'). The attribute was on the button element. mode was undefined. The trace wire button silently did nothing.

The second failure was accordion panels. Their toggle logic was:

$('.accordion-header').on('click', function() {
    $(this).next('.accordion-body').slideToggle();
    // 'this' needed to be the clicked header
});

This used a regular function correctly. But the CSS for .active state was in a different section of the monolith that was moved to a separate CSS file during the split. The toggle added .active to the element; the CSS for .active was in a file that was not loaded at that point in the HTML. The panels visually appeared broken.

The third failure was theme initialization. The dark mode toggle set a class on document.body. After the split, the toggle module loaded before the theme initialization module. The theme module read localStorage.getItem('theme') and set the class. The toggle module read the current class from document.body to set its initial state. Because initialization order was not guaranteed, the toggle sometimes read the class before the theme module set it.

What Was Actually Wrong

Shared closure scope in the monolith masked three categories of problems:

  1. this binding: Arrow functions used $(this) and happened to work because the outer this contained the expected data. The coincidence ended when modules changed what this referred to.
  1. Load order: The monolith was a single script block. Everything initialized in order. Modules loaded in any order the HTML specified. Implicit dependencies on load order became explicit failures.
  1. CSS scope: CSS was inline in the same file as the JS. After extraction to separate CSS files, rules needed to be included in the right order. Two rules in the monolith with accidental order-dependency broke when they were in different files with different load positions.

What Got Deleted

The monolith itself. 2500 lines of HTML/CSS/JS became 11 files: 2 CSS files and 9 JS modules organized into core/, canvas/, and features/ directories.

The deletion also cleared the test surface for the three bug classes: the broken arrow functions, the initialization race, and the CSS load-order issues were all visible and fixable once isolated into their own files.

What Replaced It

Module files with explicit exports and imports. Each module owns its state. Imports are explicit. The initialization order is determined by a top-level init function in core/svgEditor.js that calls each module's init in the correct sequence.

The arrow function handlers were converted to regular function declarations. The CSS was loaded in a fixed order by the HTML. The initialization race was resolved by explicit sequencing.

The Lesson

A monolith does not hide bugs by preventing them. It hides them by providing the environment they need to not manifest. When the environment changes (a module split), the bugs become visible. Splitting is not the cause. The delay is.

Read this post in the full Engineering Journal →