The 6-Hour Rewrite That Saved the Web: How Ryan Dahl Killed Node.js, Built Deno in Secret, and Admitted His Biggest Mistakes
In 2018, Ryan Dahl returned to JSConf EU with a confession: Node.js, his creation that powered millions of servers, was fundamentally broken. Then he announced he'd been secretly rebuilding it from scratch.
The 6-Hour Rewrite That Saved the Web: How Ryan Dahl Killed Node.js, Built Deno in Secret, and Admitted His Biggest Mistakes
June 6th, 2018. Berlin's Kosmos venue. Ryan Dahl walked onto the JSConf EU stage for the first time in eight years.
The last time he'd been here, in 2009, he'd unveiled Node.js โ a runtime that would go on to power Netflix, PayPal, Uber, and millions of servers worldwide. He'd become a legend. Then he'd disappeared.
Now he was back. The audience expected a victory lap.
Instead, Ryan opened with ten words that made every Node.js engineer in the room go cold:
"I have a lot of regrets about Node.js."
What followed was the most brutal technical autopsy in JavaScript history โ and the announcement of Deno, a complete reimagining of server-side JavaScript that would fix every mistake he'd made the first time.
The Golden Age That Wasn't
To understand what Ryan was confessing, you need to understand what Node.js was in 2009.
Back then, JavaScript lived in the browser. Server-side code was Java, PHP, Ruby, Python โ languages with threads, blocking I/O, and complexity that made scaling a nightmare. Ryan saw something different: JavaScript's event-driven model could be perfect for servers if you could just run it outside the browser.
He took Chrome's V8 engine, wrapped it in C++, added an event loop powered by libuv, and created Node.js. Non-blocking I/O. Single-threaded concurrency. Callbacks everywhere.
The demo was electric: a few lines of JavaScript creating a web server. No Apache. No Tomcat. Just require('http') and you were live.
By 2011, Node.js was the hottest backend technology on the planet. Ryan became Joyent's golden boy. Then, in 2012, he left. Not just Joyent โ he left Node.js. Handed the keys to Isaac Schlueter and disappeared.
For six years, the community wondered: where did Ryan go? What was he working on?
The answer: he was watching his creation grow into something he never intended. And he hated what he saw.
The 10 Regrets That Broke Node
On that Berlin stage, Ryan put up a slide titled "Design Mistakes in Node."
Ten bullet points. Each one a confession.
Regret #1: Not sticking with Promises. Node 0.2 had Promises. Ryan removed them in favor of callbacks because Promises weren't "standard" yet. By 2018, Promises were the standard โ but Node was locked into callback hell. Millions of lines of code built on fs.readFile((err, data) => { ... }). The pyramid of doom was his fault.
Regret #2: Security. Node scripts could access anything โ your filesystem, network, environment variables โ with zero permission model. require() could execute arbitrary code from node_modules. Every npm install was a potential supply chain attack. There was no sandbox. Ryan called it "a major oversight."
Regret #3: The Build System (GYP). Node used Google's GYP for native modules. GYP was designed for Chromium's massive C++ codebase. For Node? "Unnecessarily complex," Ryan said. Building native modules was a nightmare. Python 2.7 required. Hours of compiler errors. The friction killed innovation.
Regret #4: package.json. It seemed convenient โ a single file describing dependencies. But it centralized too much. It required npm. It made Node's module system depend on a centralized registry. "There's no reason why Node should be opinionated about package management," Ryan admitted.
Regret #5: node_modules. The algorithm was simple: walk up the directory tree until you find node_modules. But the result was chaos. Gigabyte-sized folders. Nested dependency hell. Version conflicts. The "heaviest object in the universe" joke wasn't a joke โ it was a design flaw.
Regret #6: require('module') without extensions. Why could you write require('foo') instead of require('foo.js')? "Convenience," Ryan said. But it forced Node to guess โ check for foo.js, then foo.json, then foo.node, then foo/index.js. Unnecessary module loader complexity. Breaks browser compatibility. "Not how browsers work."
Regret #7: index.js. Same problem. require('./foo') magically checking for ./foo/index.js was "unnecessary" complexity. It broke the principle of explicit imports.
The audience was silent. These weren't small nitpicks โ these were foundational design choices. Choices that had spawned entire ecosystems. Webpack existed partly because of module resolution complexity. Babel existed partly because of callback hell. TypeScript existed partly because JavaScript had no native type system and Node had no structure.
Ryan wasn't done.
The Secret Project
"So I've been working on something," Ryan said.
He put up a new slide: Deno.
Anagram of Node. Pronounced "dee-no." A new runtime. Built from scratch. "A fresh take on server-side JavaScript."
The audience leaned forward.
Ryan had spent two years rewriting everything. Not patching Node โ replacing it. Same core idea (V8 + event loop), but fixing every regret.
First: Security by default. Deno scripts run in a sandbox. Want to read a file? You need --allow-read. Want to make a network request? You need --allow-net. Every permission is explicit. Supply chain attacks become impossible unless you explicitly grant access.
Second: TypeScript as a first-class citizen. Node required Babel or ts-node. Deno? It runs TypeScript natively. No build step. No tsconfig.json required. Just write .ts files and run them. The type checker is built into the runtime.
Third: ES Modules everywhere. No more require(). Deno uses standard import statements โ the same syntax browsers use. Want to import a module? Use a URL:
import { serve } from "https://deno.land/std/http/server.ts";
No package.json. No node_modules. No npm install. Dependencies are URLs. They're cached globally (like browsers cache scripts). Version pinning happens in a deps.ts file or a lock file. The module system is decentralized.
Fourth: Built-in tooling. Node made you install ESLint, Prettier, Jest, Webpack. Deno has it all built-in: deno fmt, deno lint, deno test, deno bundle. One binary. Standard tools. No decision fatigue.
Fifth: Browser compatibility. Deno implements web standard APIs โ fetch(), addEventListener(), FormData, crypto. Code that runs in Deno can run in the browser with minimal changes. Node's http.createServer() was Node-specific. Deno's APIs are universal.
Ryan showed a demo. A web server in 6 lines:
import { serve } from "https://deno.land/std/http/server.ts";
serve((req) => new Response("Hello World"), { port: 8000 });
No npm init. No package.json. No node_modules. Just run deno run --allow-net server.ts.
The audience was stunned.
The Backlash Nobody Expected
Ryan expected skepticism. He got rage.
Node engineers felt betrayed. "You built Node, convinced us to bet our careers on it, then abandoned it to say it's broken?"
The npm team was furious. Deno made npm obsolete. Years of work on the registry, security audits, funding models โ suddenly irrelevant.
Open source maintainers panicked. Thousands of npm packages. Would they have to rewrite everything for Deno? Was Node being deprecated?
Ryan tried to clarify: Node wasn't going away. Deno wasn't a replacement โ it was an alternative. A chance to fix the mistakes without breaking the ecosystem.
But the damage was done. The community split. Node loyalists vs. Deno early adopters. Heated GitHub issues. Blog posts with titles like "Why Deno is Wrong" and "Why Node is Dead."
The technical arguments were fierce:
- URLs for imports? What about versioning conflicts? What about offline development?
- TypeScript built-in? That's a massive runtime dependency. What about compile times?
- No package.json? How do you manage complex dependency graphs?
- Security flags? That's just more CLI friction.
--allow-read --allow-net --allow-envfor every script?
Ryan and the Deno team (Bartek Iwaลczuk, Kitson Kelly, Ryan's co-creators) pushed back with real answers. Lock files for version pinning. Remote caching for offline work. Import maps for aliasing. Permission inheritance for complex apps.
Slowly, the ecosystem started to form.
The Long Game
Today, five years later, Deno hasn't killed Node. Node is still the dominant server-side JavaScript runtime.
But Deno changed Node.
Node.js v14 added experimental ES Module support. Node.js v16 stabilized it. Node.js v18 added native fetch(). The fetch API came from Deno's pressure. Node's security model improved. Package imports got more explicit.
Deno, meanwhile, evolved. Deno 1.0 shipped in May 2020. Deno Deploy launched (edge runtime, like Cloudflare Workers). Deno 2.0 added npm compatibility โ you can now use npm packages in Deno without node_modules.
Vercel hired Ryan Dahl. Then the Deno team raised $21M. Then they launched Deno Subhosting (multi-tenant sandboxed JavaScript execution). Companies like Netlify, Supabase, and Slack started using Deno for edge functions.
The architecture story is fascinating:
- Written in Rust (not C++ like Node), using Tokio for async I/O instead of libuv
- V8 isolates for sandboxing โ each script runs in a separate V8 context with explicit permissions
- Built-in TypeScript compilation via swc (a Rust-based compiler), cached in
~/.cache/deno - HTTP/3 support via QUIC, native WebSockets, and built-in WebAssembly integration
- No centralized registry โ modules are fetched via HTTPS, cached locally, integrity-checked via lock files
The technical depth is staggering. Deno's module loader doesn't walk directories โ it resolves URLs, fetches remote code, caches it cryptographically (SHA-256), and verifies integrity on every run. The permission system uses capability-based security at the syscall level. The TypeScript integration bypasses tsc entirely, using swc for transpilation and V8's built-in type stripping.
The Legacy
Ryan Dahl's confession in Berlin wasn't just a mea culpa. It was a masterclass in technical leadership.
He didn't defend his mistakes. He enumerated them. He didn't ask for forgiveness. He built a solution. He didn't demand the community follow him. He showed them a better way and let them choose.
Node.js today is better because Ryan admitted its flaws. Deno exists because Ryan believed in starting over. The JavaScript ecosystem is richer because one engineer had the humility to say: "I was wrong, and here's how we fix it."
The lesson isn't "rewrite everything." It's "know when to."
Sometimes the bravest thing you can do is look at your greatest success, see its cracks, and build something new โ knowing full well the world might reject it.
Ryan did it anyway.
And in the process, he showed every engineer that legacy isn't about protecting what you built. It's about building what comes next โ even if it means admitting your first try wasn't perfect.
Node.js runs the world. Deno runs the future. And somewhere between them, Ryan Dahl proved that the best way to honor your creation is to be willing to break it.
Keep Reading
The 48-Hour Gamble That Made React: How Jordan Walke Secretly Rewrote Facebook's Newsfeed in a Rogue Programming Language Nobody Wanted
In 2011, a Facebook engineer built a JavaScript framework that his own team called 'weird' and 'too different.' Two years later, it would rewrite the rules of web development forever.
The Database That Wasn't Supposed to Win: How PostgreSQL Outlived Oracle, MySQL, and MongoDB by Refusing to Compromise
In 1996, two Berkeley professors released a database nobody wanted. Twenty-eight years later, it's beating billion-dollar companies at their own game โ and the story of how it survived is stranger than fiction.
The 'Second Best' Language That Won Everything: How a Christmas Hobby Project Became the World's Most Popular Programming Language
In December 1989, Guido van Rossum was bored during Christmas break and wanted to build a language that was actually fun to use. Nobody expected his weekend project to conquer AI, web development, science, finance, and DevOps โ despite being 100x slower than its competitors.