All Mixed Up: Isomorphic Sorting Gone Wrong

All Mixed Up: Isomorphic Sorting Gone Wrong

At 4/19/2024

Illustration of interface cards being sorted by icon

As a JavaScript developer, I’m used to my code running differently in different environments. But earlier this week, I ran into a problem that left me scratching my head for most of the morning.

I was working on an isomorphic JavaScript application and noticed that the JavaScript sort() function was returning different results on a Node server compared to in the browser.

Here’s a test case illustrating the problem. We’re fetching 30 of Cloud Four’s GitHub repos, sorting them by programming language, and logging them to the console:

// Fetch our data from the Github API
fetch("https://api.github.com/users/cloudfour/repos")
  .then(resp => resp.json())
  .then(repos => {
    // Remove all the extra fields besides name and language
    const cloudFourRepos = repos.map(({ name, language }) => ({
      name, 
      language: String(language),
    }))
    // Sort by programming language
    .sort((a, b) => {
      if (a.language === b.language) {
        return 0;
      }
      return a.language < b.language ? -1 : 1;
    });

    console.table(cloudFourRepos);
  });
Code language: JavaScript (javascript)

Whether I run this in Node Footnote 1 or the browser, the sort() function correctly groups the repos by language. But inside of those groups, things start to get a little weird.

To figure out what’s going on, let’s drill down and only look at the HTML repos. Here they are unsorted, as well as sorted by Node and the browser. Footnote 2 The browser has left them in the same relative sort order, while Node has scrambled them all up!

Unsorted Sorted in Browser Sorted in Node
cloudfour.com-patterns cloudfour.com-patterns pwa.rocks
drizzle drizzle cloudfour.com-patterns
leveller leveller drizzle
pwa-workshop pwa-workshop pwa-workshop
pwa.rocks pwa.rocks leveller

What’s going on here? Why is the same sort() function behaving so differently in different environments?

It turns out that when the compare function returns 0 (two elements match), it’s up to the JavaScript engine to choose how to handle it!

If compareFunction(a, b) returns 0, leave a and b unchanged with respect to each other, but sorted with respect to all different elements. Note: the ECMAscript standard does not guarantee this behavior, thus, not all browsers (e.g. Mozilla versions dating back to at least 2003) respect this.

The difference we’re seeing here is called “sort stability.” A stable sort function will leave the order of the elements the same if they match under the sorting criteria. An unstable sorting criteria on the other hand, is likely to rearrange those matching elements.

Older versions of Node (everything before Node 11) and older browsers used unstable sorting algorithms while most modern browsers use stable sorts.

Sometimes stable sorting doesn’t matter, but it’s important to be aware of the difference.

Stable sorting can allow you to chain sorting functions, sorting first by one characteristic and then by another.

Also, it can be confusing for users if a page renders differently depending on whether it’s rendered on the server or in the browser. With some isomorphic application frameworks this can lead to hydration errors and — in one particularly odd bug I ran into recently — switch the photos on user profiles.

We have a few options to ensure our sort option returns the same array regardless of the JavaScript engine we’re using and whether it uses a stable sort.

One way to make our sort stable is to loop over the array and add keys to each item based on their index. This allows us to sort by their index after sorting by our primary criteria. Here it is in Node and the browser:

const cloudFourRepos = repos.map(({ name, language }, index) => ({
  name,
  language: String(language),
  index,
}))
// Sort by programming language
.sort((a, b) => {
  if (a.language === b.language) {
    return a.index < b.index ? -1 : 1;
  }
  return a.language < b.language ? -1 : 1;
});
Code language: JavaScript (javascript)

If we’re not worried about stability, but just want our sort to match between Node and the browser, we have another option. If our array items already have a unique key like an ID, we can sort by that property after sorting by our primary sort criteria. Our Github repos have IDs so we can do this in Node and in the browser:

const cloudFourRepos = repos.map(({ name, language, id }) => ({
  name, 
  language: String(language),
  id,
}))
// Sort by programming language
.sort((a, b) => {
  if (a.language === b.language) {
    return a.id < b.id ? -1 : 1;
  }
  return a.language < b.language ? -1 : 1;
});
Code language: JavaScript (javascript)

Debugging this issue was a good reminder to always consider the environment your code is running in and to question your assumptions about how things work.

From now on whenever I sort objects in JavaScript I’ll consider whether I need stable sorting and whether my sort function will work the same in every environment.

Footnotes

  1. To use fetch in Node you’ll need to install and require node-fetch Return to the text before footnote 1
  2. The list of repos may have changed since this article was written.  Return to the text before footnote 2
Copyrights

We respect the property rights of others and are always careful not to infringe on their rights, so authors and publishing houses have the right to demand that an article or book download link be removed from the site. If you find an article or book of yours and do not agree to the posting of a download link, or you have a suggestion or complaint, write to us through the Contact Us, or by email at: support@freewsad.com.

More About us