From Development

Deno vs Node - comparison (2020)

I don't know if you're up with the news, but Deno is the new star on the Javascript Ecosystem - some people even say it's going to kill Node. Now that Deno is released, it is time to find out if it can be a worthy competition for Node. Curious? Let's start!

In this post, we will take a look at Deno and how it compares to Node to help understand what they have in common, and what sets them apart in terms of:

  • TypeScript
  • Security
  • Modules
  • Package management
  • Promises
  • Browser compatibility

I hope we can help you having a clear though on what its best for you.

At Imaginary Cloud, we have been using Node to build APIs, serve frontends, and to build microservices architectures, among others.

What is Node?

Node is a server-side JavaScript environment based on Google’s V8 JavasScript engine, created by Ryan Dahl in 2009 and was heavily focused on event-driven HTTP servers. It brought server-side JavaScript to the mainstream and it was this JavaScript everywhere paradigm that allowed the development of web applications using a single programming language.

Fast forward to 2018, Ryan Dahl gave a talk in JSConf EU entitled Design Mistakes in Node - also known as “10 Things I Regret About Node.js”. In his talk, he details his regrets regarding some of the choices that were made in the development of Node. As he points out, at the time Node started being developed, JavaScript was a much different language, and it lacked some of the most modern features:

  1. Promises and Async/Await
  2. ES Modules
  3. TypeScript

Considering many of the design flaws mentioned at the talk couldn’t be fixed without rewriting the core of Node and thus ending the support for legacy applications, Ryan decided to introduce Deno.

What is Deno?

First of all, Deno is not a fork of Node - it’s a new implementation based on modern features of the JavaScript language, although the name is an anagram of Node. Deno is a secure runtime for JavaScript and TypeScript based on Google’s V8 and its core is built in Rust (Node’s implementation is in C++). It uses Tokio for its event loop, which is also written in Rust.

How to install it

# Mac/Linux 
brew install deno

# windows
choco install deno

For other installation methods, check the official documentation.

(At the time I’m writing this, the available Deno version is 1.4.0).

How Deno is different from Node

Single Executable File

Deno ships as a single executable with no dependencies and comes with some built-in tools to make the developer experience easier:

  1. bundler (deno bundle)
  2. debugger (--inspect, --inspect-brk)
  3. dependency inspector (deno info)
  4. documentation generator (deno doc)
  5. formatter (deno fmt)
  6. test runner (deno test)
  7. linter (deno lint) - currently unstable

All these tools are standardized for Deno, so you have sure that they will have full support.

Being a single executable, Deno can update itself via: deno upgrade or deno upgrade --version <new_version>.

This will fetch the specified version (or latest if unspecified) and replace your current executable with it.

It's possible to have multiple versions installed, using version managers.

For Node, version managers are also responsible for updating/installing new versions, e.g nvm.

First Class TypeScript

Deno is based on TypeScript, so it supports it out of the box without the need to install or configure tools, like adding a tsconfig.json configuration file in Node. Although it comes with a default config file, it's possible to add your own deno run -c tsconfig.json [file-to-run.ts].

Since TypeScript is just a superset of JavaScript, Deno can also run it.

// hello-world.ts 
const sayHello = function (greeter: string) {
  return `Hello from ${greeter}`;
};

console.log(sayHello([..."node"].sort().join(""))); 

To run a script, you can create a hello-world.ts file with the snippet above and run it by executing: deno run hello-world.ts. This will use Microsoft’s TypeScript compiler to check types and produce JavaScript and then run the script.

Right now, Deno's team assumes that the time it takes V8 to parse TypeScript compared to JavaScript is much higher and it's an ongoing task to port the TypeScript Compiler to Rust.

Security

One of the main focuses of Deno is security. Deno is secured by default and the code is executed in a secure sandbox, replicating the same permission model that the browser implements. Unless specified, it has no access to the filesystem, network, or environmental variables, and the access permissions need to be explicitly passed to the Deno process on the command line.

// env-deno.ts
console.log("Hello", Deno.env.get("USER"));

If you try to run the previous snippet:

❯ deno run env-deno.ts 
Check file:///env-deno.ts
error: Uncaught PermissionDenied: access to environment variables, run again with the --allow-env flag
    at unwrapResponse (deno:cli/rt/10_dispatch_json.js:24:13)
    at sendSync (deno:cli/rt/10_dispatch_json.js:51:12)
    at Object.getEnv [as get] (deno:cli/rt/30_os.js:32:12)
    at file:///Users/torrao/Projects/work/deno/env-deno.ts:1:31 

To avoid the error, you need to add the --allow-env flag to be able to have access to the environmental variables:

❯ deno run --allow-env env-deno.ts 
Hello torrao

There's an option to allow all access --allow-all or -A, but this is not recommended.

Diversely, Node is very permissive, you have full access to pretty much everything:

// env-node.js
console.log(process.env.USER);

Modules

Deno uses ES Modules, the official standard format to package JavaScript code, introduced in ES6/ES2015:

import package from 'module-name'

When Node was created, JavaScript didn't have it's own module system, so it used the CommonJS standard:

const package = require('module-name')

Right now Node only has experimental support for ES Modules and you need to do some configurations/changes to existing files.

I will not go into detail about the differences, but if you want an in-depth look at both module systems I recommend reading this.

Although not being compatible, in general, with Node packages, Deno provides a Node Compatibility Library, that will allow us to use some NPM packages that do not use non-polyfilled Node APIs.

One of the main advantages is that uses a standard browser-compatible protocol for loading modules, allowing you to import modules directly via URLs, which has a huge impact in the way we handle modules.

Package Management

Considering that Deno uses URLs for loading modules, it explicity takes on the role of both runtime and package manager. It’s not dependent on a centralised server for modules distribution.This means it requires fully qualified module names, including the extension (or a server providing the correct media type).

You can import modules directly and use dependencies as code:

// http-server.ts 
import { serve } from "https://deno.land/std/http/server.ts"; 

for await (const req of serve({ port: 8000 })) {
    req.respond({ body: "Hi from Deno!\n" }); 
} 

To run the previous snippet, you can just execute: deno run --allow-net http-server.ts. There is no install to do beforehand because Deno downloads and caches a module in a global directory the first time its URL is encountered in a script. The modules cache has two main goals:

  • offline work: if the cache already exists for a specific module it uses it. You can add the —reload flag if you want to manually reset the cache.
  • single copy: every specific module version that is imported, only has a copy, no matter how many projects reference it.

All modules and files loaded from remote URLs, apart from being cachable are also intended to be immutable.

As you probably noticed, imports require fully qualified module names, including the extension. Besides, Deno has no "magical" module resolution. Instead, imported modules are specified as files (including extensions) or fully qualified URL imports.

Node uses npm as the package manager to install and manage third-party packages listed in the npm registry. This makes linking to external libraries fundamentally centralized through the npm registry. When you install a package into your project with NPM/Yarn, a package.json file is used to specify the package name and accepted versions, and the packages are downloaded into a node_modules folder inside your project. As a result, node_modules folders become huge, since modules require specific versions of other modules, and they will be replicated in every single project directory it requires.

Deno does not use NPM for dependency management, so no package.json and no node_modules

Standard Modules

Deno provides curated standard modules of helpers and utilities for common tasks that are audited by the Deno Core team.

These modules will be tagged in accordance with Deno releases. You can link to a specific version tagged version to v0.72.0 using the URL https://deno.land/std@0.72.0. Not specifying a tag will link automatically to the master branch, so it's recommended that you link to tagged versions to avoid unintended updates and breaking changes.

In case you want to lock a specific module version, you can use a lock.json file, to check module integrity.

Third Party Modules

Deno provides a hosting service integrity for Deno scripts. It caches releases of open source modules stored on GitHub and serves them at one easy to remember domain.

For instance, if you are looking for a date utility you can use the https://deno.land/x search function that helps you find modules. Once you’ve found the module you want, in this case, date-fns, you can just import the correct URL in the file you’re using it in:

import { format } from "https://deno.land/x/date_fns@v2.15.0/index.js"; 

format(new Date(2014, 1, 11), "yyyy-MM-dd"); 

For all the other NPM packages, if they are not compatible with esmodule imports, you can use a service like JSPM which will resolve the 3rd party modules and compile the CommonJS modules to work as ES Modules imports.

Promises

Deno uses promises all the way down - all asynchronous methods return Promises:

// read-file.ts - Deno 
try { 
    const data = await Deno.readFile('README.md'); 
    // Handle the data 
} catch (e) { 
    // Handle the error 
}

From the previous snippet, you can see that Deno supports top-level await. This allows us to use the await syntax in the global scope without having to wrap it in the async function. Recently Node Node also added support for top-level await.

Long before Promises or async/await, Node's API for asynchronous operations were designed to use callbacks and follow the error callback convention:

// read-file.js - Node 
const fs = require('fs'); 
fs.readFile('README.md', (err, data) => { 
    if (err) { // error handling } 
    else { // data handling } 
}); 

Even though Node developers now have access to Promises and the async/await syntax, Node's API still expect callbacks to maintain backward compatibility.

Probably one big difference is that Deno always dies immediately on unhandled promises, where Node currently handles rejections by emitting a deprecation warning to stderr. Right now, Node is running a survey to better understand the way users are dealing with this.

Browser compatibility

The development team also decided to use browser APIs where it’s practical to do so. Like so, Deno provides a global window object, and APIs such as addEventListener, onload, onunload and fetch.

Deno provides a global window object, and APIs such as addEventListener, onload, onunload and fetch.

This means that Deno programs written completely in JavaScript which do not use the global Deno namespace (or feature test for it), ought to be isomorphic, meaning they will be able to be run in a modern web browser without any change:

import capitalize from "https://unpkg.com/lodash-es@4.17.15/capitalize.js";

export function main() {
  console.log(capitalize("hello from the web browser"));
}

window.onload = () => {
  console.info(capitalize("module loaded!"));
};

While the window object is not available in Node, you can use fetch if you polyfill this or use a third-party library.

Which to choose: Deno vs Node?

The goal of Deno is not to replace Node, but to offer an alternative.

Node has been under development for over a decade, which makes it more stable and battle-tested, making it the de facto standard for server-side JavaScript. Deno has only been under development for just two years and continues to be improved. Among other things, Deno is an excellent replacement for utility scripts that are usually written with bash or python.

It will depend on the requirements, but for the majority of Node applications, Deno may not be a fit right now. One of the main hurdles to tackle is the creation/conversion of NPM modules to be used with Deno and this will probably change in the future when Node ES Modules support will become more standard.

However, I think we will gradually see Deno get adopted more and more because of its first-class TypeScript support and modern standard library. Node programmers should keep an eye on Deno and maybe try to use it for some side projects.

One thing is sure, Deno's current development is pushing the JS server ecosystem forward and that's a good thing.

Found this article useful? You might like these ones too!

At Imaginary Cloud, we simplify complex systems, delivering interfaces that users love. If you’ve enjoyed this article, you will certainly enjoy our newsletter, which may be subscribed below. Take this chance to also check our latest work and, if there is any project that you think we can help with, feel free to reach us. We look forward to hearing from you!