---
title: How Upgrading Ruby Broke JavaScript
teaser: A tale of false assumptions.
tags: ruby,javascript,debugging
author: Matheus Richard
published_on: 2023-04-18
---

I was working on a Rails app and finished all my tasks. I wanted to be
proactive, and a new Ruby minor version was available (3.2.1). It was almost 5
PM, what better time to upgrade my app?

These minor version upgrades are simple, right? I bumped the Ruby version and
the CI Docker image, hit bundle install, and the tests passed locally. All good
news. I pushed the branch to open a PR, and then... CI broke. What?

## The beginning of the pains

I checked the error in CI, and it's a system spec failing. Weird! Everything
passed locally. The error said Capybara couldn't find a select option. I looked
at Capybara's error screenshot, and the expected text was there!

Okay, desperate moments require desperate actions, so I used an [advanced
debugging technique][advanced debugging technique]: printing to the CI console.
I printed the text of the option tag, and it... looked exactly what the test
asserted. But strangely, it returned `false` when I compared the `option.text` to
the expected text.

In disbelief, I copied the option text from the CI logs to my editor when suddenly
I saw a highlighted char. One of the spaces in the text wasn't a normal blank
space but a [`U+202f`] char.

[advanced debugging technique]: https://tenderlovemaking.com/2016/02/05/i-am-a-puts-debuggerer.html
[`U+202f`]: https://en.wikipedia.org/w/index.php?title=Non-breaking_space&useskin=vector#Narrow_nonbreaking_space

## It's not a bug. It's a feature!

Why on Earth did that character end up there? I certainly didn't add it! I checked
how that select options were being created. Each option content was just a
`Time` object converted to a string with Rails translation helpers, nothing
fancy. Except, there was a Stimulus controller attached to them. It was a
controller called `local-time`, which allowed users to see the time values in
their local time. This controller looked like this:

```js
import { Controller } from "@hotwired/stimulus"

const locale = document.documentElement.lang
const dateFormatter = new Intl.DateTimeFormat(locale, { timeStyle: "full" })

export default class extends Controller {
  connect() {
    const datetime = new Date(this.element.value)
    const formattedDatetime = dateFormatter.format(datetime)

    this.element.textContent = formattedDatetime
  }
}
```

<aside class="info">
  <p>
    If you're wondering why the app didn't use the
    <a href="https://github.com/basecamp/local_time">local_time</a> gem, it's
    because it doesn't work with tags other than the <code>&lt;time&gt;</code>.
    There is
    <a href="https://github.com/basecamp/local_time/issues/129">an open issue</a>
    about that.
  </p>
</aside>

So the error had something to do with JS. After some unlucky Google searches, I
tried searching "Google Chrome U+202f" and found [this Stack Overflow answer].

It looks like Chromium [changed] how [`Intl`] date parsing worked. But it
eventually undid that because it was causing [too much trouble][] (Firefox had
done [something similar] too).

Well, that explained why the test passed on main (using older Chrome) and
locally (newer Chrome) but not on that specific branch (a version of Chrome with
that behavior).

[this stack overflow answer]: https://stackoverflow.com/questions/75406192/javascript-tolocaletimestring-returning-ascii-226-instead-of-space-in-latest-v/75406644#75406644
[changed]: https://chromium.googlesource.com/v8/v8/+/2ada52cffbff11074abfaac18938bf02d85454f5
[`Intl`]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl
[too much trouble]: https://bugs.chromium.org/p/chromium/issues/detail?id=1414292
[something similar]: https://bugzil.la/1806042#c17

## The fix

This is not super important for this article, but it would feel incomplete
without it. The best solution was simply upgrading Chrome on CI, which
eliminates that undesired behavior altogether.

If that was not possible, for some reason, we could modify the `local-time`
Stimulus controller to replace all instances of the `U+202f` char with a normal
space:

```js
const formattedDatetime = dateFormatter.format(datetime)
                                       .replaceAll("\u202F", " ")
// ...
```

## The lesson

Although there's a chance of someone getting to this article and finding a way to
fix their bug, I think there's something more important for us here.

**When a bug happens, oftentimes it comes from false assumptions**. In this
particular case, I had several assumptions while fixing the bug:

1. I initially assumed that I was just upgrading Ruby, when in fact, I happened
   to upgrade CI's Docker file, which upgraded Chrome as well.
2. I assumed that the text in the option tag was equal to the test
   expectation just because they *looked* the same.
3. I implicitly assumed that the browser had the same behavior in different
   versions.
4. I also assumed my local Chrome instance behaved exactly like the one in CI.

Sometimes, a bug is the culmination of a lengthy chain reaction of falling
dominoes. We have to go back and find what caused the initial topple. That is
why it's important to [be aware of the assumptions you're making] while
debugging. If that topic interests you, we made [a whole series] about it!

[be aware of the assumptions you're making]: https://thoughtbot.com/blog/debugging-listing-your-assumptions
[a whole series]: https://thoughtbot.com/blog/tags/debugging-series-2021
