What happened when JavaScript did not

A compact menu hid the catalogue before its control was ready, turning progressive enhancement from a preference into an execution-order requirement.

C62Y’s navigation worked without JavaScript until JavaScript started and failed.

The baseline document contained a complete list of links. The enhancement added a compact button at narrow widths and collapsed the list. I considered the no-script case covered because the unenhanced HTML was usable.

The script applied an enhanced class near the beginning of initialization. CSS used that class to hide the list. A later line attempted to attach the click handler and failed in an older browser after encountering an unsupported method.

The page now had the worst combination: JavaScript existed, so the baseline disappeared; JavaScript had not completed, so the replacement behavior did not exist.

I had designed two endpoints—script and no script—and ignored the states between them.

Failure happened inside initialization

My mental model treated enhancement as one operation:

plain navigation → compact navigation

The browser executed several observable steps:

find region
add enhanced class
create button
insert button
attach behavior
set initial state

Any step could fail. The order determined what remained.

Adding the class first made later success a requirement for access. I had allowed an optional enhancement to revoke a dependable baseline before proving it could replace it.

The fix was not a catch block that restored the list for this one error. I changed the construction order so every prefix of the sequence remained usable.

The script verified its dependencies, created and configured the button, attached behavior, inserted the control, and only then applied the class that enabled collapsed styling. If any earlier step failed, the links stayed visible.

Progressive enhancement became a transaction with a safe commit point.

I had used a broad check for JavaScript availability: the script loaded and ran. That said nothing about whether the required DOM methods, event model, or selectors behaved as expected.

I replaced the assumption with narrow capability checks at the boundary where the feature needed them. The navigation did not attempt enhancement unless it could create the button, find the region, attach the required event, and expose state.

Capability detection did not prove the complete behavior. A method can exist and contain a bug. It reduced the number of predictable failures and kept unsupported environments on the complete baseline.

I avoided using browser names to decide. A new browser could share an old limitation, and an old browser could support the exact methods the feature required. The question was whether this behavior could be constructed safely here.

The capability check lived beside the enhancement, not in one global modernBrowser flag. Different features had different needs. A browser could receive the compact navigation and not receive an unrelated optional image behavior.

This prevented support from becoming one ladder on which every feature rose and fell together.

The enhanced class had originally meant “JavaScript is enabled.” After the failure, it meant something more precise: the navigation control and behavior are installed.

That semantic change simplified the CSS. Rules that hid the link list applied only under a state that could reveal it again. The baseline styles never depended on script removing a no-JavaScript class quickly enough to prevent a flash or trap.

The control also managed an explicit expanded state. CSS responded to the state and did not infer it from an inline display property written by several code paths.

The class became a small receipt. It did not prove the feature would never fail later, but it recorded that initialization had crossed its safe boundary.

I began using enhancement markers sparingly and specifically. A global class on the root element could still help where many styles depended on one verified capability. It should not become permission to hide content across the page before individual features were ready.

The name of a class can encode an architectural promise. js was too broad. nav-disclosure-ready described the actual commitment.

A control without behavior was still a defect

Reordering initialization left another partial state: the button could be inserted and then a later step could fail before the ready class appeared. The links remained visible, so navigation was possible, but the dead button was confusing.

I assembled the button off-document and attached its behavior before insertion. The code still had a tiny interval where an unexpected failure could leave it behind. A cleanup path removed the uncommitted control if setup threw.

More importantly, the button’s presence did not hide anything by itself. A dead optional control beside visible navigation was a defect and not a loss of the primary task.

This helped me distinguish failure severity. Resilience does not mean every partial state looks polished. It means optional machinery fails without taking the durable capability with it.

The release test still treated the dead control as a bug. The architecture limited consequence while the test and diagnostics helped remove the cause.

That difference would recur later in distributed systems: safe failure and successful behavior are not the same standard.

The navigation was only the first feature

I audited C62Y’s other scripts for the same pattern.

The comparison picker hid its server-rendered form before building the enhanced selector. The image viewer replaced an ordinary link with a lightbox trigger before verifying that the overlay could initialize. A filter marked the product list busy and could leave it that way after an exception.

Each enhancement received a safe sequence:

  • Start from a complete server-rendered action.
  • Build the enhanced control without removing the baseline.
  • Attach and test the necessary state transitions.
  • Commit the enhanced presentation.
  • Preserve an escape or ordinary navigation path where appropriate.

Not every enhancement could be tested fully before it became interactive. The important part was that the baseline did not disappear because a script had merely begun.

The audit removed one enhancement entirely. The custom image viewer added focus, resize, and failure behavior around a link whose normal destination already showed the full image. The ordinary link was better.

Progressive enhancement became a tool for deciding not to enhance.

Network success did not guarantee execution

I had treated script failure as a missing file. The initialization defect showed many other possibilities:

  • The file loaded and contained a syntax unsupported by the browser.
  • An earlier script threw and prevented later initialization.
  • A dependency loaded after the feature expected it.
  • An extension or policy blocked one resource.
  • The DOM differed from the assumed template.
  • A selector returned no element after a content change.
  • Storage access or another browser API threw despite existing.

A successful HTTP response was not evidence that the feature became ready.

I added small development diagnostics around each enhancement’s initialization and made missing required hooks visible in the test environment. Production did not expose stack traces to readers; it preserved the baseline and recorded a bounded error where the privacy and logging setup allowed.

The distinction suggested a wider rule: an open connection would not prove its source was current, just as a loaded script did not prove an interaction was installed.

Transport and application state were already beginning to separate.

Server rendering made the baseline concrete

C62Y’s server-rendered pages contained real headings, links, forms, and comparison URLs. The baseline was not a loading shell waiting for JavaScript to become an application.

That made progressive enhancement easier to reason about. The browser received a complete task. Script could improve disclosure, filtering, and selection without becoming the sole owner of content.

I did not interpret this as an argument against rich client applications. It was an argument for identifying which state and behavior truly required the client.

The catalogue’s core navigation and reading did not. A filter could update without a full page load and still preserve a query URL and form submission. The comparison selector could improve in place and still link to a server-rendered result.

The baseline also simplified testing. I could request a URL, inspect the document, and verify the task before a browser executed anything.

Later universal JavaScript work would make this boundary look more fashionable and more complicated. C62Y established the simpler lesson first: HTML can carry product state worth preserving.

Error handling could not restore unknown state casually

My first patch wrapped initialization in try/catch and removed the enhanced class on error. That was better than hidden links and too blunt.

If failure happened after event handlers attached or state changed, simply removing one class could leave duplicate controls, stale ARIA state, or listeners operating against visible baseline content.

The construction order reduced how much rollback was needed. Setup occurred off-document where possible. The commit point happened once. After commit, runtime errors followed a separate recovery path.

For navigation, runtime recovery could safely expand the list, disable the disclosure button, and keep links available. For the comparison filter, a runtime failure could preserve the current results and reveal the ordinary form. Neither tried to infer and reverse every internal operation.

This was a small version of a rule I later applied to workflows: design the state transitions so recovery is an ordinary transition, not an attempt to undo an arbitrary sequence of side effects.

Good error handling begins with a system whose partial states are named and limited.

The navigation script originally lived in the document head and ran before the region existed. A library wrapper delayed initialization until the DOM was ready, which obscured the dependency under another abstraction.

I moved small enhancement scripts after the relevant document content or used a clear ready boundary. They did not block initial text and links from parsing.

Optional scripts loaded after the critical document and styles. A failure to fetch them left the main task intact. The asset inventory showed which features spent requests before first use.

I avoided combining every script into one opaque bundle solely to reduce requests. In the network conditions of the time, request count mattered, but one error in a monolithic initialization path could still prevent unrelated enhancements. A small, ordered bundle with isolated feature setup gave a better balance for C62Y.

The project later adopted build tooling and hashing, but loading order remained a product choice: what must exist before a person can navigate, read, compare, or recover?

The compact navigation opened correctly with a pointer after the initialization fix. Keyboard testing found that the script listened only for click behavior on the fake anchor used by the first version.

Replacing it with a button restored native keyboard activation. The code became smaller because the platform supplied behavior I had been imitating.

Escape handling introduced another runtime path. If focus was inside the open menu, Escape closed it and returned focus to the button. If focus was elsewhere, the script did nothing. The handler did not intercept Escape globally.

When an exception occurred during close, the recovery path kept the links visible. Focus return was helpful and subordinate to access.

These tests reinforced a broader connection: semantic HTML reduces the amount of JavaScript that must execute correctly. Resilience and accessibility often share the same dependency reduction.

The most robust key handler was the one the button did not need me to write.

The no-script test was not enough

I expanded the failure matrix:

  • No script requested.
  • Script request fails.
  • Script parses but capability check fails.
  • Initialization stops before control creation.
  • Initialization stops after control construction but before commit.
  • Feature commits and then a runtime transition fails.
  • DOM hook is missing after a template change.
  • Wide and narrow layout changes while the feature is open.

I used deliberate switches in development rather than hoping to interrupt execution manually at the right line. Each switch produced a known partial state.

The matrix made the architecture’s claim testable. Before commit, the baseline stayed intact. After commit, recovery returned the primary task even if the enhancement became unavailable.

It also exposed where the claim was too strong. The comparison enhancement could not safely reconstruct a form after arbitrary DOM mutation, so the ordinary form remained in the document and the enhanced layer decorated it instead of replacing it.

Testing failure changed implementation rather than merely checking it.

Progressive enhancement was not nostalgia

I sometimes heard progressive enhancement framed as preserving an old web of simple documents. C62Y made it feel more current than that.

The catalogue was being opened on new devices, under uncertain networks, with a widening range of browser capabilities. Starting from meaningful HTML gave the product more ways to succeed in that future.

Enhancement still mattered. Compact navigation made narrow use better. In-place filtering reduced delay. A comparison picker made exploration faster. The principle did not reduce the interface to its baseline.

It set an authority boundary: optional code could add convenience after it was ready, and it could not revoke the core task merely by existing.

That boundary was architectural, not aesthetic. A richly designed interface could follow it. A visually plain page could violate it by hiding everything behind a fragile loader.

Enhancement respected a person’s chosen state

One compact-menu revision reset itself after every filter update because the script reran initialization on replaced content. A user could open the navigation, submit a nearby form, and receive a page that forgot the disclosure state without explanation.

I made initialization idempotent and scoped it to regions that were actually new. An already enhanced navigation was recognized by its committed marker and not rebuilt. The current expanded state stayed attached to the existing control.

For full navigation, a new page load began from the predictable collapsed default at narrow widths. I considered persisting the preference and decided the state was too transient to justify storage. The important requirement was consistency within the current page and no duplicate controls after partial updates.

This added another meaning to readiness: an enhancement should not only install safely; repeated initialization should not multiply behavior or erase deliberate user state. Modern client applications would make that lifecycle more formal. The small catalogue already contained the problem.

I treated JavaScript as either present or absent and ignored partial execution. The hidden navigation made the missing middle visible.

I used a global js class as permission to hide feature content before individual features were ready. Specific commit markers replaced it.

I inserted a control before its behavior was attached. Off-document construction and cleanup reduced the dead-control state.

I added a catch block that tried to reverse unknown setup. Reordering the transition made rollback smaller and runtime recovery explicit.

I used a link as a button and wrote behavior the platform already provided. Semantic controls removed code and failure paths.

I kept one image-viewer enhancement because it looked sophisticated. The ordinary high-resolution link served the task with less risk, so I removed the viewer.

The mistakes all came from assuming the enhanced state was the product and the baseline was a temporary scaffold. C62Y worked better when both were real, with a safe transition between them.

The durable lesson was execution order

Progressive enhancement is often summarized as layers: HTML, CSS, JavaScript. The failure taught me to think in transitions.

A dependable baseline exists. Optional code checks what it needs. It constructs the replacement without destroying the baseline. It commits only after the replacement can uphold the task. Runtime failure has a bounded recovery path.

The sequence felt reusable beyond this navigation: keep the known-good state, prepare the candidate beside it, verify the candidate, and only then change which state owns the task.

The mechanisms became more sophisticated. The underlying correction remained: never remove the known-good state merely because the next state has started trying to exist.

The next C62Y article leaves the browser and asks whether the same content hierarchy can survive paper. Print exposed assumptions that responsive screen testing had not: link destinations, page breaks, interactive controls, and a comparison wider than the physical sheet.