On the 13th of May 2020, a very promising step forward was taken in the JavaScript community. The first stable release of Deno, which has been in development for over 2 years, was released. Deno has been touted as a fresh approach to some of the mistakes and issues that occurred in the original development of NodeJS back in 2009.
This blog post aims to cut through a lot of the hype you may see around Deno in an attempt to present the facts - so even a complete beginner to the JavaScript or NodeJS ecosystem can understand Deno and its impact.
What is Deno?
Deno is a JavaScript runtime created by Ryan Dahl, the original creator of NodeJS. It was originally announced by Ryan at JSConf EU 2018, born out of a desire by Dahl to improve and rethink certain aspects of the JavaScript runtime he originally created.
On the deno website, the tagline reads:
Deno is a simple, modern and secure runtime for JavaScript and TypeScript that uses V8 and is built in Rust.
What does this actually mean? Let's break this down, and clear up some of the jargon.
A JavaScript runtime is an environment which contains everything you need to execute a program written in JavaScript. In the case of NodeJS, the runtime is made up of 2 high level concepts:
- The V8 JavaScript engine. A JavaScript engine compiles and executes your JavaScript code. V8 is a specific JavaScript engine written in C++ created and maintained by Google and used to power NodeJS, Chrome and Chromium.
- APIs and modules. These include modules for interacting with the file system, making HTTP requests, timers etc.
So with this in mind, we are in a much better position to understand Deno and how it is different to Node.
Deno is a runtime that still uses the V8 engine to run JavaScript code. Deno differs because it's written in rust rather than C++. This includes the CLI and all the nice tooling that Deno provides, which come packaged up in a single, executable file. The rust code interfaces with the V8 engine through the rusty_v8 rust package (or crate, as they are known in the rust ecosystem). The rust crate Tokio is used heavily in deno under the hood for asynchronous behaviour and scheduling.
The modules and APIs in Deno are written in TypeScript which is a first class citizen of the runtime. TypeScript code is compiled into JavaScript internally in Deno. This allows developers to write TypeScript and have it compiled and run as if it was just JavaScript, without installing any additional tooling.
Why Deno?
So we now know how the actual runtimes differ. But why? What was wrong with the current one in Node? Why did Ryan Dahl and the rest of the deno team spend 2 years developing this? Ryan Dahl himself goes into detail about this here. He details the regrets he had about building Node in the first place, which laid the groundwork for deno. Ryan announces Deno in this talk, albeit in it's very early stages.
If you don't want to watch the video, or haven't got time. I respect that. Let's summarise the points.
Regret: Not Sticking with Promises
NodeJS is built on the concept of callbacks. Callbacks are functions that are run when a task has finished executing. For example, when you read a file using the fs
module:
const fs = require("fs");
fs.readFile("index.html", function(err, data) {
if (err) {
console.log("Error Reading File!", err);
}
console.log(data);
});
The function passed as the second argument to fs.readFile
here is the callback. This function will be run whenever node has finished reading your file and will return either the data (if successful) or an error (if something went wrong). Callbacks have since been eclipsed by promises, which provide a simpler and more powerful API for dealing with asynchronous behaviour in JavaScript.
Later, it became possible to convert functions with callbacks to use promises using the promisify
function on the util
module in Node. Here's the promisified version of the above example.
const util = require("util");
const fs = require("fs");
const readFilePromise = util.promisify(fs.readFile);
readFilePromise("index.html").then(data => {
console.log(data);
})
.catch(err => console.error("Error Reading File!", err))
A little verbose, but it does the job. We can further improve this code by using the async
/await
feature, which aimed to make asynchronous JavaScript code read like synchronous code. Here's the above example rewritten to use async/await
.
const util = require("util");
const fs = require("fs");
const readFilePromise = util.promisify(fs.readFile);
async function readFile() {
try {
const data = await readFilePromise("index.html");
} catch (err) {
console.error("Error reading file!", err);
}
}
readFile();
Promises were actually added to Node in June 2009, but removed again in February 2010 to keep things simple in the NodeJS codebase. This was a regret of Ryans, as the unified usage of promises would have sped up the delivery and standardisation or async/await
.
Deno supports promises out of the box and each of it's asynchronous APIs were written with Promises in mind. Not having to mix and match promises and callbacks provides a simpler, more consistent asynchronous programming experience. Let's see how we perform the same task as above using the deno standard library.
const file = await Deno.open("index.html");
try {
await Deno.readFile(file, Deno.stdout);
} catch (err) {
console.error("Error reading file!", err);
}
file.close();
No callbacks. All the deno async APIs will return a promise to us.
Regret: Security
Node has access to several things by default that could be considered sensitive. This includes the file system, network and system calls. Dahl wishes that he had put more thought and effort into security in NodeJS. The example he gave was running a linter. Linters like ESLint probably should not have full access to your computer and network. Unfortunately, they do.
Deno, however, is secure by default. The runtime will not allow you to access the file system, or perform network calls unless you explicitly give it permissions. We'll get into some examples of this later on.
Regret: The Build System (GYP)
According to Ryan Dahl, the NodeJS build system is his biggest regret.
As much as we'd all like to, we cannot write everything in JavaScript. Sometimes we need much better performance than JavaScript can provide. This is where we need to compile native modules written in lower level languages like C++.
If you are writing a NodeJS module that interfaces to a C++ libary, your code will make use of GYP (Generate Your Projects). GYP and more specifically node-gyp is a tool that is used to compile native nodeJS modules written in C or C++ so they can be accessed with require
, like any other JS module.
Node originally used GYP because Google Chrome used to use GYP heavily. The chrome team later switched over to GN. GN aims to be a more readable and maintainable file format than GYP. It also offers significantly better performance.
This left Node in limbo as the sole user of GYP. Deno seeks to solve this by using a mixture of GN and cargo, the build system for Rust. This provides much better build performance and more friendly APIs for compiling native modules.
Regret: Package.json
package.json
is the cornerstone of modern JavaScript. It includes the dependencies for your project and tells Node what to install when you run an npm install
. NPM is a centralised repository of JavaScript packages and it is where you will download a vast majority of JS packages from. As JavaScript developers, we generally do not go for long without having to interact with NPM in one way or another.
Dahl regrets package.json
for several reasons. He does not like the fact that you have to define your dependencies in 2 places - package.json
and also inside your code when you require
the module. He also regrets the fact that this is not an abstraction that exists on the web, since you can just add a <script />
tag with a URL to your HTML.
package.json
also contains a bunch of other information, such as the name, description and repository of your project. Dahl calls this "noise" and believes that it is unneccessary.
Deno looks to solve this by removing the concept of package.json
altogether. Deno allows you to require modules directly from URLs, drastically simplifying the whole concept of requiring external modules in your scripts and applications.
Regret: node_modules
So package.json
says which modules you need, but the modules themselves are installed in your node_modules
directory inside your project. Every project has it's own node_modules
folder, requiring you to duplicate dependencies across projects. This concept deviates a lot from the way dependencies work in the browser.
Deno takes a fresh approach to this. node_modules
does not exist in Deno. When you require an external module in Deno, it is downloaded and your dependencies are stored in a specific directory defined by you. Your program will not fetch the same modules again however, as deno caches dependencies for you.
Regret: require("module") without extension (".js", ".ts")
In Node, you can require files without the .js
extension. This creates the need to query the file system and check what you actually meant when the node module loader is resolving your modules. Ryan argues that this is less explicit and deviates from the way it's done in the browser, which looks something like the following:
<script src="myscript.js" type="text/javascript"></script>
Deno enforces the .js
or .ts
extension on imports to alleviate this.
Regret: index.js
In Node, if you require
a module, index.js
is generally used as an "entry" file to a module or application. This is similar to how index.html
is resolved by default in a web server. This means you can require a folder like this.
const mymodule = require("./my-cool-module")
Node will look for a index.js
file inside the my-cool-module
directory by default to resolve the module. The same is true for modules imported from node_modules
. Dahl regrets this because it is not explicit, but more practically, it complicated the node module loading system by introducing another implicit step to check for the existence of index.js
.
Installing Deno
It's pretty simple to get up and running with deno. On OSX, simply run:
brew install deno
This command should install the single deno executable on your machine. You can check that worked by running:
deno --version
If that didn't work, or you are on another OS, check out the deno installation guide.
Features
Now that we have deno up and running, let's dive into some of the shiny new features.
TypeScript Support
TypeScript is a first class citizen of Deno. This means that we can write our code in TypeScript without having to wory about adding any tooling for our compilation step. When we run TypeScript code with deno, it will type check and compile it for us and run it as if it were normal JavaScript. This completely removes the overhead of setting up the TypeScript compiler in your project.
No NPM - Deno Packaging
Deno takes a fresh and different approach to importing external code. It has no package manager. You can run some external code by simply passing a URL to the deno run
command.
deno run https://deno.land/std/examples/welcome.ts
This command will pull down the module from the internet, compile it, cache it indefinitely and run it, all without NPM. If you want to import
a module and use it in your code, you don't have to install a module from NPM and require
it. Let's say we create the following file, test.ts
.
import { assertEquals } from "https://deno.land/std/testing/asserts.ts";
assertEquals(2, 2);
We can then run this with:
deno run test.ts
Our module will resolve modules from the web and run the code. The next time you do this, deno doesn't have to fetch the module again as it is cached. We can run code from any publicly accessible URL. For example, if you want to run some code directly from github, you could use the following command:
deno run https://github.com/shogunpurple/deno-test/blob/master/src/hello.ts
Secure Runtime Permissions
Deno is a secure runtime. This means that it operates on the principle of only giving permissions that you supply explicitly. This differs to NodeJS in that node automatically gives permissions to things like your file system and network. Some things that deno will not let you do without explicitly enabling permissions are:
- Read/Write from the File System
- Make HTTP requests and access the network
- Run subprocesses
To demonstrate this, let's work through an example. We are going to run a script that reads a file on the file system.
$ deno run https://deno.land/std/examples/cat.ts /etc/passwd
We get the following output:
$ deno run https://deno.land/std/examples/cat.ts /etc/passwd
error: Uncaught PermissionDenied: read access to "/etc/passwd", run again with the --allow-read flag
at unwrapResponse ($deno$/ops/dispatch_json.ts:43:11)
at Object.sendAsync ($deno$/ops/dispatch_json.ts:98:10)
at async Object.open ($deno$/files.ts:37:15)
at async https://deno.land/std/examples/cat.ts:4:16
Whoops. This happens because deno doesn't allow us to read from the file system unless we specify file reading permissions in our deno run
command.
$ deno run --allow-read=/etc https://deno.land/std/examples/cat.ts /etc/passwd
This works! You will now see the contents of the /etc/passwd
file in your terminal output. This provides a powerful layer of security that allows us to configure only the exact permissions we want our program to have when it's run. Some other examples of things you may want to give your program permissions for when running are:
--allow-net
- allow HTTP requests from your program--allow-env
- allow access to environment variables.--allow-run
- allow your program to run sub-processes.
Most of these flags take arguments, allowing you to restrict access to particular resources. For example, if you only want to allow your program to send HTTP requests to https://myapi.com:
deno run --allow-net=https://myapi.com my-program.ts
Standard Library
The limited standard libary in Node has always been a bit of a sticking point for developers. There is heavy reliance on external modules to perform tasks in Node that are included in the standard library of many other programming languages. UUID generation is an example of this, where the uuid library is the de facto solution for Node developers, however it is not part of the node standard library. Deno provides an extensive standard library based on that of the go programming language that includes some nice functionality such as:
- date/time functions
- http servers
- logging
- permissions
- testing
- uuid generation
- websockets
- file system utilities (fs)
- hashing and cryptography
- parsing command line flags
Being able to do this stuff without external modules gives deno the ability to build a myriad of different applications with just the standard library.
Browser Compatible APIs
Also included in the deno standard library are an impressive set of APIs that adhere to web standards and therefore can be run in the browser, allowing code written and compiled with deno to be run on the client as well as the server!
One of the most important ones that deno includes is an implemention of fetch
, a browser API used for making HTTP requests. In node, you need to import an external module for this like node-fetch
, or use the native http
module in node, which is a little clunky and verbose. If we wanted to use fetch
to make a call to google.com with deno, we could use the following code:
const response = await fetch("http://www.google.com");
console.log(response);
Also notice how we are able to use a top level await
here - another feature deno supports out of the box. Let's run our code above with:
deno run --allow-net fetch_google.ts
The result of our HTTP call to google will be shown in the console. Notice how we specified the allow-net
permissions to allow our code to make an HTTP request. Here are some other common browser APIs that deno supports.
addEventListener
removeEventListener
setInterval
clearInterval
dispatchEvent
You can see the full list of web compliant APIs in deno here.
Complying to web standards will make the deno API much more future proof and provides utility for front-end developers.
Deno Tools
As well as the actual language features above, deno gives us additional tooling that performs tasks currently carried out by the likes of webpack, rollup, and prettier. The difference is that deno includes these tools out of the box.
Bundling
Bundling is the process of taking your application and dependencies, and outputting it into a single JavaScript file that can be executed. This job is generally performed by module bundlers such as rollup, webpack and parcel. Deno gives us a simple approach for bundling code with the deno bundle
command. If we want to bundle some code, we can do the following with deno.
$ deno bundle https://deno.land/std/examples/echo_server.ts server.bundle.js
Bundling https://deno.land/std/examples/echo_server.ts
Download https://deno.land/std/examples/echo_server.ts
Warning Implicitly using master branch https://deno.land/std/examples/echo_server.ts
Emitting bundle to "server.bundle.js"
2661 bytes emmited.
We can now run our bundle like any other normal script.
$ deno run --allow-net server.bundle.js
Listening on 0.0.0.0:8080
Built in Testing
Deno has a built in test runner that allows us to test our JavaScript and TypeScript code. If you are familiar with JavaScript testing libraries such as Jest or Jasmine, this syntax will look familiar.
Deno.test("deno test", () => {
const name = "John";
const surname = "Wick";
const fullname = `${name} ${surname}`;
assertEquals(fullname, "John Wick");
});
We use the test
functionality on the Deno
namespace to create a test. We can then run our tests with the deno test
command:
$ deno test test.ts
Compile file:///Users/martinmckeaveney/Development/deno-test/.deno.test.ts
running 1 tests
test deno test ... ok (4ms)
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out (5ms)
This is a powerful feature, as you could just run a full test suite by pointing your deno test
command to a test suite hosted somewhere online, without having to pull down and run the test suite yourself.
Script Installation
Packaging your scripts up into a single executable is a very useful thing when you want someone to be able to run your script on their machine without installing Node. Currently if you want to do this with a node script, this can be done with the fantastic pkg module by vercel, which requires you to install that external module through NPM.
Deno provides a built in script installer, allowing you to distribute your scripts as a single executable. Let's see how it works. We will install the basic deno "Hello World" script from the deno.land website as an executable script.
$ deno install https://deno.land/std/examples/welcome.ts
Download https://deno.land/std/examples/welcome.ts
Warning Implicitly using master branch https://deno.land/std/examples/welcome.ts
Compile https://deno.land/std/examples/welcome.ts
✅ Successfully installed welcome
/Users/martinmckeaveney/.deno/bin/welcome
ℹ️ Add /Users/martin/.deno/bin to PATH
export PATH="/Users/martin/.deno/bin:$PATH"
Scripts are saved by default to the .deno/bin
folder located in our home directory. We can execute our script directly:
$ ./.deno/bin/welcome
Welcome to Deno 🦕
As mentioned in the message above, if we want to execute this script from anywhere on our system, we need to add the .deno
directory to our PATH
. The PATH
variable tells our machine where to look for scripts when we execute them from the terminal. Once we add the .deno
directory to our path, we can run the script from anywhere!
$ welcome
Welcome to Deno 🦕
Formatting
Prettier is the de facto formatter for JavaScript code. Deno provides a built in formatter (that actually uses prettier under the hood) through the deno fmt
command.
We can format some ugly looking code in this test.ts
script by running deno fmt
on it.
import { assertEquals } from "https://deno.land/std/testing/asserts.ts";
Deno.test("deno test", () => { const name = "John"; const surname = "Wick";
const fullname = `${ name} ${surname}`;
assertEquals(fullname, "John Wick"); }
)
;
$ deno fmt
Our result is nicely formatted code.
import { assertEquals } from "https://deno.land/std/testing/asserts.ts";
Deno.test("deno test", () => {
const name = "John";
const surname = "Wick";
const fullname = `${name} ${surname}`;
assertEquals(fullname, "John Wick");
});
deno fmt
provides a nice utility by default that can be a little bit of effort to set up in standard JavaScript projects. Some other commands supports by deno fmt
include:
deno fmt --check
- check if files are already formatteddeno fmt file1.ts
- format specific files
Downsides
A radical simplification and completely different approach to a JavaScript runtime is not without its limitations.
Backwards Compatibility with Node
Deno is not the "next version" of node. It is a brand new implementation that is no longer compatible with NPM packages and existing node modules. The https://deno.land/std/node/ API is being built to allow for this, but at the time of writing this is not complete. There are quite a few ports of existing node modules on the deno website that you can use. Check them out on the deno.land website. As Deno matures, this list of libraries ported over for deno support will grow exponentially.
TypeScript Compiler
Deno uses the TypeScript compiler to check types and compile your code into JavaScript. This provides a bottleneck where your code must go through that extra compilation and type checking step before it runs. The TypeScript compiler is written in TypeScript, which means type checking performance in TypeScript will never match the raw performance of native languages like C++ or Rust. There are potential plans to implement the type checking for deno in rust, which is a huge project and won't be ready for quite a long time.
This limitation primarily applies to much larger projects, where the time compiling TypeScript code becomes more of an issue as the codebase grows. For most small to medium projects, this will not be an issue for most developers.
Conclusion
The internet loves dichotomies.
A lot of people want to know if they should drop the thing they are learning to move to the hot new one on an almost daily basis, for fear of being left behind. You can see evidence of this with the regular reddit posts and tweets screaming the title "is
Deno is not a replacement for NodeJS. There are too many node programs that currently exist, with many more being written every hour of every single day. Deno is a fresh take and new ethos towards building, packaging and distributing scripts and applications in the JS ecosystem, built on modern technologies with a particular focus on providing a powerful scripting environment with the tooling you need built in.
Deno still has a long way to go. In order for Deno to become a success, the community must use it and build modules for it. Deno must find it's place in the daily workflow of JavaScript developers.
One of the problems up to this point with modern JavaScript is the heavy reliance on tooling. Although JS tooling has opened doors that were never thought possible in the past, it can be overwhelming for beginners and even experienced developers who aren't familiar with the JS ecosystem. Deno hides a lot of this away and makes those decisions for you. This is a huge plus for a lot of engineers.
I hope you learned something. Thanks for reading!
Feel free to reach out to me or follow me on Twitter, where I tweet and blog about JavaScript, Python, AWS, automation and no-code development.