A Threshold Number I Discovered in a DOM API

#Editor#JavaScript#Browser

Subtitle: the meaning of 0.99999999999994315...

Introduction

When you build an editor, you tend to run into unexpected problems pretty often.

Last time, I shared an issue I ran into while drawing the caret myself. Once you've drawn the caret, the next step is positioning it accurately on the text. In this post, I want to share a peculiar browser issue I bumped into during exactly that step.

It's a problem that came up with the caretRangeFromPoint API — something you'll almost inevitably use when you implement a caret yourself — and it's also deeply tied to floating-point arithmetic. (That said, the issue no longer reproduces today..!)

The problem: an odd, intermittent caret jump

When a paragraph spans more than one line, clicking on the empty space at the end of the last line caused the caret to land in the wrong place.

  • Expected: caret lands at the end of the text on the last line
  • Actual: caret jumps to the line above, at roughly the same x coordinate as the click

I was using the caretRangeFromPoint API to position the caret.

What is caretRangeFromPoint?

caretRangeFromPoint(x, y)

  • Takes (x, y) coordinates and returns the nearest text range
  • Note: display: block elements are ignored (my guess is because they don't contain glyphs directly)

The most baffling part of this bug was that it didn't reproduce consistently. At first it looked intermittent, but on closer inspection, the bug repeated only at certain coordinates. In other words, at the problematic positions it always misbehaved, and at the working positions it always behaved correctly.

First guess: a character problem

Within the same width, the bug appeared only when the last character was a, not when it was e. So my first suspicion was that the glyph itself was the culprit. But the logs told a different story — the issue wasn't the shape of the glyph but the decimal pattern of the coordinate where that glyph ended.

Second guess: page width / layout problem

Next, I suspected page width (responsive layout, line-break point) was at fault. When I changed the page, some widths reproduced the issue easily and others didn't. But across different layouts, the common thread was that the bug only kept repeating when the decimal portion of the boundary coordinate matched a particular pattern.

It turned out to be a number (decimal) problem

In the end, the issue wasn't characters or layout — it was the coordinate number itself.

When the user clicks empty space, to nudge the caret toward the nearest text, I grab the text's boundary value with getBoundingClientRect(), correct the coordinate, and pass that corrected coordinate back into caretRangeFromPoint. Because of the tiny fractional error introduced during that correction, the API couldn't find the nearest text and ended up selecting text on the line above.

On a "just in case…" hunch, I tried rounding, and the symptom vanished.

But rounding without a reason wasn't something I could ship. A text editor has to respect even a 1-pixel difference from the user. So I only used rounding to confirm the hypothesis and kept chasing the root cause.

The real cause: the moment I found the threshold

To reproduce the bug, I started testing coordinate values one by one.

// Spotted a strange pattern
caretRangeFromPoint(100.99999999999991); // ❌ broken
caretRangeFromPoint(100.99999999999998); // ✅ works
caretRangeFromPoint(100.99999999999993); // ❌ broken
caretRangeFromPoint(100.99999999999996); // ✅ works
caretRangeFromPoint(100.99999999999994); // ❌ broken
caretRangeFromPoint(100.99999999999995); // ✅ works
// Narrowing in on the threshold — probing the 94 range more finely
 
caretRangeFromPoint(100.999999999999941); // ❌ broken
caretRangeFromPoint(100.999999999999948); // ✅ works
caretRangeFromPoint(100.999999999999943); // ❌ broken
caretRangeFromPoint(100.999999999999944); // ✅ works
// Narrowing in further — probing the 943 range more finely
 
....

To track down the cause, I went down a rabbit hole of swapping coordinate values one at a time. I tweaked the click coordinate by 0.1, then 0.01, even by 0.000001 — dozens of iterations. And finally — I discovered that caretRangeFromPoint consistently misbehaves only for specific number patterns.

For example, when I hit a value like 562.99999999999994315..., that coordinate started acting like a boundary threshold, and instead of returning a proper text Range, the API returned null.

  • To the left (smaller values) of this threshold, caretRangeFromPoint returns null
  • To the right (larger values) of this threshold, it returns a normal Range

So the API couldn't handle decimal values that landed right on the boundary, which is why the caret kept either jumping to the line above or coming back as an empty Range (null).

What really stood out was that the threshold wasn't 1 but a very long fractional part (e.g., nnn.99999999999994315...). Once a number got that long, the API was clearly treating it like an internal boundary threshold.

That led me to the conclusion: "Ah, when the browser processes coordinates internally, it's using fractional parts like nnn.99999999999994315... as threshold values."

How the issue unfolds

  1. User clicks empty space → capture the click coordinate (x, y)

  2. Coordinate correction → use getBoundingClientRect() to get the text box's boundary, then correct the click coordinate based on that value

    • At this point the value is usually something reasonable like 562.234893248
  3. Store the corrected value → save the corrected coordinate into a variable (adjustedX)

    • When the variable gets read or used in further math, JavaScript's floating-point behavior surfaces a long fractional tail like 562.2348932480000001 or 562.23489324899994315…
  4. API call → pass this stretched value into caretRangeFromPoint(adjustedX, y)

  5. Misbehavior

    • If the coordinate lands on a boundary threshold, the API fails to produce a proper Range
    • To the left of the threshold it returns null or the caret jumps to the line above
    • To the right of the threshold it returns a normal Range

How I fixed it

The fix was surprisingly simple.

  1. Round the value As I'd already verified, rounding the coordinate at a sensible decimal place removes the long fractional tail and lets caretRangeFromPoint work normally.

  2. Shift the coordinate by an ε Since specific decimal values act like boundaries, you can step the coordinate inward from the element's boundary by ε and then call the API.

In the end, I went with option 2.

 adjustedX -= Math.ceil(0.99999999999994315);

Rounding alone would have fixed it, but I had no principled answer for "which decimal place do we round at?", and it might cause side effects elsewhere later. Shifting inward by ε, on the other hand, was the more reasonable choice because it named the cause and addressed it directly.

Conclusion

This issue was a tangled mix of a DOM API and floating-point arithmetic. Searching Google for keywords like caretRangeFromPoint bug or caretRangeFromPoint pointer-events none turned up nothing relevant, so I had to track down the cause myself.

During debugging, I was inspecting numbers with more than 17 decimal digits one by one, and somewhere in that endless stream of 0.999999..., when a digit other than 9 (a 4) finally appeared as in 0.99999999999994, that was the moment I felt I'd finally caught the thread. It wasn't just a meaningless string of 0.9999s — the change in the significant digits was the root cause of the issue.

Of course, if I'd just papered over it with rounding, I could have moved on faster. But then I'd never have known the real cause, and I would have been left with that unsettled feeling.

That said, on my current setup (OS: macOS, browser: Chrome), the issue no longer reproduces. DevTools also doesn't expose numbers with more than 17 digits anymore, which I assume is due to the IEEE 754 floating-point representation. I never managed to pin down the exact conditions that triggered it in our company environment, but if I ever hit a similar DOM API issue in the future, I now have a solid reason to suspect tiny numerical errors all over again.