If you've been working with JavaScript for more than a few years, you've probably encountered the following error:
SyntaxError: Unexpected token 'export'
Every few months this error pops up, and I start fighting with Jest, TypeScript, and ES modules, applying mysterious configuration fixes that I find online without really understanding why. These solutions always involved "cryptic" configuration changes, but these solutions never included an explanation of the underlying problem.
Recently, while struggling with a d3-delaunay
import that worked perfectly with ts-node
but exploded in Jest, I finally discovered both the root cause and a much better solution. More importantly, I learned something about when to dig deeper as an engineer, and when to just use the tool that works.
JavaScript ES Modules vs CommonJS: The Core Difference
The root of all this pain comes down to one fundamental difference that's rarely explained clearly:
CommonJS (Node's original module system) loads modules synchronously at runtime:
const fs = require('fs'); // This actually executes code to load the module
ES Modules are static—imports and exports are analyzed before any code runs:
import fs from 'fs'; // This is just a declaration, resolved at compile time
This isn't just a syntax difference. It's a different execution model. When Node.js encounters an export
statement in CommonJS mode, it's trying to execute that line of code, but export
isn't executable, it's a static declaration that should have been analyzed during the parsing phase.
That's why you can't just swap require
for import
and call it a day. The runtime needs to switch modes too.
Why Tools Struggle With This
Understanding this static vs. dynamic difference suddenly makes most of these "cryptic" configuration changes make sense. Some common solutions and what they really mean:
"type": "module"
in package.json → "Hey Node, treat this package as static ES modules"- Transform patterns in bundlers → "Convert ES modules to CommonJS at build time"
--experimental-vm-modules
→ "Use a different execution engine that understands both"
All these changes are basically telling tools how to bridge the two fundamentally different worlds of CommonJS and ES Modules.
Jest's Retrofit Problem vs. Vitest's Fresh Start
This is where my issue comes into focus. I had build a NodeJS backend server, that was running fine when executed with ts-node
. Next, I wanted to write some unit tests for it using the JavaScript testing framework: Jest. After writing tests for my functionality that was using the npm library d3-delaunay
, I ran into some strange errors.
While debugging this issue I noticed that d3-delaunay
is an ES module. Which seemed to be the root cause of my issues. I tried many of the above described "magic" configuration changes, but none of them worked. Then, I switched to Vitest, and everything just worked.
It kind of makes sense. Jest was built during the CommonJS era and has been retrofitting ES module support ever since. That's why working with ES modules in Jest feels like fighting the framework, because you essentially are. You're asking a CommonJS-native tool to pretend to understand ES modules through a complex series of transformations and workarounds.
Vitest, on the other hand, was built ES modules-first. When you import d3-delaunay
in Vitest, it just works. No configuration, no transform patterns, no experimental flags. Vitest also handles CommonJS modules seamlessly, you get the best of both worlds without the configuration headaches. It's not magic, it's just a tool designed for the module system you're actually using.
This is a perfect example of how legacy in software compounds. Jest's early success meant lots of adoption, which meant lots of pressure to maintain backwards compatibility, which meant ES modules was an afterthought, rather than a design decision.
The Rabbit Hole Dilemma: When Not to Dig Deeper
Here's where this story becomes about more than just JavaScript modules.
As engineers, our instinct is often to understand everything deeply. The thought process goes: "I should probably learn how bundlers work," or "Maybe I should write my own module loader to really understand this." While that curiosity is generally a superpower, it can also be a productivity killer for me.
The truth is: you don't need to understand every abstraction you use. You need to understand enough to be effective, and to recognize when you need to go deeper versus when you need to just use the tool that works.
Save the deep dives for when you have time or when a project specifically needs that level of understanding. There's always more fascinating rabbit holes than time to explore them!
In this case, knowing that ES modules are static (not dynamic) gives you enough mental model to understand why tools struggle. The key is building just enough understanding to make informed tool choices, not to become an expert in every layer of the stack.
Summary
JavaScript's module ecosystem can feel like a minefield, but the core issue is actually quite simple: CommonJS and ES modules are fundamentally different execution models, not just different syntax.
Tools that were built for one system struggle with the other. Jest retrofitted ES module support; Vitest was built ES-first. That's why one feels like fighting the framework while the other just works.
As engineers, we don't need to understand every implementation detail. We need enough understanding to make good choices. Sometimes the right choice is switching tools rather than fighting configuration.
And sometimes the best engineering decision is to move on to the actual problem you're trying to solve, rather than getting lost in trying to understand the very tools that were designed to hide the complexity in the first place. The irony.
After writing this post, I've updated my NodeJS backend starter to use Vitest instead of Jest. For me this means: no more module configuration headaches for future projects!

Simon Karman
Simon Karman is a cloud engineer and consultant that builds effective cloud solutions ranging from serverless applications, to apis, to CICD pipelines, and to infrastructure as code solutions in both AWS and GCP.
Contact