Ryan Dahl's Node.js Regrets — and Why He Built Deno

In 2009, Ryan Dahl gave the world Node.js. Nine years later, at JSConf EU 2018, he returned to the same stage and did something almost unheard of in tech: he listed, publicly and methodically, everything he got wrong. Then he showed what he had built to fix it.

This is a companion piece to the Ryan Dahl tribute article. If you haven't watched the original 2009 and 2010 talks, start there — the regrets land harder when you understand what he was trying to build in the first place.

The Talk: 10 Things I Regret About Node.js (JSConf EU 2018)

The title is a slight misdirection — the talk covers around seven substantial regrets, not exactly ten. But the framing is precise: Dahl is not complaining. He is performing a root-cause analysis on his own work, nine years after the fact, with the benefit of watching millions of developers use it in production. That is a rare and useful perspective.

The Regrets, One by One

1. Not Sticking with Promises

Dahl added Promises to Node's core in 2009, then removed them in 2010 in favor of raw callbacks. He reversed this because Promises felt like unnecessary overhead at the time. In retrospect it was the wrong call — Promises were the necessary foundation for async/await, and their absence from Node core meant years of fragmented async patterns before the ecosystem converged. Every .then() chain you wrote before async/await existed is downstream of this decision.

2. Security

Node gives every application full access to the filesystem, network, and environment variables by default. Dahl's example was pointed: a linter should not need read access to your SSH keys. There is no reason a build tool needs unrestricted network access. He wanted a sandbox — and the absence of one meant Node was never safe for running untrusted code.

3. The Build System (GYP)

GYP — Generate Your Projects — was a Google-internal build system that Node adopted for its native C++ bindings. Dahl called it "the largest failure of Node core." It is Python-based, poorly documented, and produces a deeply unpleasant experience for anyone writing native modules. He wished he had provided a clean FFI (Foreign Function Interface) instead, letting JavaScript call into C libraries without requiring users to write C++ wrappers.

4. package.json

Allowing require() to read package.json for module resolution gave birth to the concept of a "package" as a directory with a manifest. That abstraction then grew: license fields, repository fields, scripts, peer dependencies, dev dependencies, optional dependencies. Dahl's regret is not that packages exist — it's that package.json became an obligatory blob of metadata whose contents bear no relationship to what JavaScript actually needs to import a module.

5. node_modules

Perhaps the most viscerally felt regret among working Node developers. The node_modules resolution algorithm — traverse up the directory tree until you find a matching folder — is complex, non-deterministic at scale, and produces dependency trees that are famously both enormous and fragile. Dahl described it as a "massively complex" module resolution system that "unnecessarily diverges from browser semantics." The meme of node_modules being the heaviest object in the universe exists because of this decision.

6. require() Without File Extensions

require('./utils') works — Node guesses whether you mean utils.js, utils/index.js, or something else. This requires multiple filesystem lookups per import. It is opaque, inconsistent with how browsers handle URLs, and makes static analysis harder. Dahl wished he had required explicit extensions from the start: require('./utils.js').

7. index.js

Treating index.js as a directory's default entry point added yet another layer to the resolution algorithm and, more importantly, propagated a pattern with no analog on the web. A URL does not silently resolve to /index.html in the module system — but Node pretends it does. A small decision that compounded the complexity of the module loader significantly.

The Response: Deno

Rather than filing these regrets and moving on, Dahl spent the next two years building Deno — a new JavaScript and TypeScript runtime written in Rust, designed from scratch to not repeat any of the above mistakes.

  • Promises and async/await by default — the entire standard library is async, no callbacks in the API surface
  • Secure sandbox — no file, network, or environment access unless you explicitly pass --allow-read, --allow-net, etc.
  • No GYP, no node_modules, no package.json — modules are imported by URL or explicit file path
  • Explicit extensions requiredimport './utils.ts' not import './utils'
  • TypeScript built in — no transpilation step, no config files, just deno run app.ts

Deno is not a rewrite of Node. It is a considered response to it — the same core insight (non-blocking I/O, V8, JavaScript on the server) rebuilt without the accumulated debt. Whether it displaces Node at scale is an open question. But as a demonstration that the author of a platform can hold his own work to account and do something about it, it is singular.