📦 How ESM Broke Discord

Discord logo
Photo by Mariia Shalabaieva / Unsplash

Last week my Discord client on Ubuntu stopped working, seemingly out of the blue. Little did I know it was related to the highly controversial JavaScript module debate: CommonJS vs ESM.

Timeline

A quick breakdown of the timeline was as follows:

  • May 7th - GitHub releases a major version of octokit.js
  • July 24th at 11am - Renovate creates a pull request to bump octokit.js to v4
  • July 24th at 2:00pm - I created ~/package.json to experiment with Node.js support for ESM
  • July 24th at 6:00pm - I shut down my computer for the day
  • July 25th at 8:00am - Discord is broken

CommonJS

CommonJS has long been the default packaging method for Node.js applications. However, users are slowly starting to publish and migrate to ESM. One such application is GitHub's octokit.js, an SDK for GitHub. In their release notes, it mentions this:

BREAKING CHANGES
- package is now ESM

ESM in Node.js

It turns out that Node.js has quite a bit of support for ESM. However, this comes with some complexities and caveats. One of the caveats is that you cannot use require() to load a module that is explicitly packaged as ESM. This is exactly the issue I was running into while trying to upgrade Immich's discord-bot. Rennovate opened a pull request last week and I started to investigate the issue by creating a ~/package.json and ~/index.js to experiment around with ESM support in Node.js.

By the way, how does NodeJS know if a module is ESM or not? Well, it turns out that one way you can communicate that is by setting the "type": "module" in package.json. If that is set, it treats the script as an ESM.

If you did try to require() a file from an ESM package, you would get the following error:

node index.js 
file:///home/jrasm91/index.js:1
const path = require('path');
             ^

ReferenceError: require is not defined in ES module scope, you can use import instead
This file is being treated as an ES module because it has a '.js' file extension and '/home/jrasm91/package.json' contains "type": "module". To treat it as a CommonJS script, rename it to use the '.cjs' file extension.
    at file:///home/jrasm91/index.js:1:14
    at ModuleJob.run (node:internal/modules/esm/module_job:218:25)
    at async ModuleLoader.import (node:internal/modules/esm/loader:329:24)
    at async loadESM (node:internal/process/esm_loader:34:7)
    at async handleMainPromise (node:internal/modules/run_main:113:12)

Node.js v20.10.0

So basically you can't load ESM through require() and Node.js determines a script belongs to an ESM package based off of "type": "module" in the nearest package.json. I'm sure you can see where this is going 😅.

"Gray screen of death"

When I woke up the next day and turned my computer on, I was baffled to see that Discord was not working correctly. In fact, it would start up and then just display a blank, gray screen.

Broken discord client

Troubleshooting

I tried the usual troubleshooting steps:

  • Closed, and re-opened Discord
  • Launched Discord via the command line, looking for errors
  • Deleted ~/.config/discord
  • Deleted ~/.cache/discord
  • Uninstalled, and re-installed Discord
  • Verified discord.com continued to work just fine
  • Re-installed Discord from the snap store
  • Launched Discord with --no-sandbox and --disable-gpu

However, all of these failed, so I reluctantly opened a support ticket on https://support.discord.com.

Eventually, I was asked to download and try their canary and nightly packages of Discord. While running those editions, I got the same error. However, I was actually able to press CTRL + SHIFT + i, since you know, it's just chrome 😆.

I had tried this before in the regular client, but the keyboard shortcut must have been disabled because it was a release version of the build. In the canary and nightly versions it opened devtools and I was able to find some useful errors in the console.

Wait, what?

Yeah, exactly. It turns out that Discord downloads some js files, stores them in ~/.cache/discord and then loads them when discord starts up using Node.js (I assume via Electron). Because there is no package.json in any of the parent folders (whyyyyyyyyy), Node.js keeps searching up the directory tree until it finds the one in my home folder. And, as you may have guessed, when set to "type": "module", prevents the file from being loaded. That leads to the very useless gray screen without any indication as to what went wrong.

Solution

Once I knew what the issue was, it was easy enough to delete the package.json file or at the minimum remove the line "type": "module". After that, everything immediately starting working correctly again. I updated the support ticket with my findings, to which they replied:

Kindly provide a hardware report of your desktop so our team can escalate this ticket for further investigation.

😆😆😆

I guess it is to be expected. And, to be fair, most developers don't fully understand the differences between ESM and CommonJS, let alone Discord support staff!

TLDR - don't add random package.json files with "type": "module" to your home folder because, who knows, it might break some random electron-based app, like Discord!