How JavaScript works: introduction to Deno

Gigi Sayfan
SessionStack Blog
Published in
11 min readMay 20, 2021

This is post # 28 of the series, dedicated to exploring JavaScript and its building components. In the process of identifying and describing the core elements, we also share some rules of thumb we use when building SessionStack, a JavaScript tool for developers to identify, visualize, and reproduce web app bugs through pixel-perfect session replay.

Overview

Deno is a secure runtime for running JavaScript and Typescript applications. In this article, we will look at the origins of Deno, compare it to Node.js, and explore various aspects like modules, packages, async support, Typescript support, security, and tooling. We will also look under the hood of Deno and how it’s implemented. But, first, let’s see Deno in action.

Lightning-quick Deno Demo

Installing Deno on the mac is as easy as:

brew install deno

For other operating systems, follow the instructions here:

https://deno.land/#installation

Deno can run your scripts, but also has a REPL.

Given the following file yeah.ts:

Deno can run it using:

$ deno run -q ./yeah.ts

Yeah, it works!!!

Let’s do the same thing in the REPL

$ deno

Deno 1.9.0

exit using ctrl+d or close()

> console.log(‘Yeah, it works!’)

Yeah, it works!

undefined

Deno has many other sub-commands besides run. Type deno -h to see them.

Now that we saw some code and typed some commands let’s take a step back and talk about where Deno came from.

Origins of Deno

Deno was announced in Ryan Dahl’s (Node.js creator) talk “10 Things I Regret About Node.js” at the JSConf EU 2018. This is pretty cool that a creator gets to see his original project skyrockets in popularity, observe it being used in the field and gets to learn from its mistakes. The typical scenario is that creators try to improve and refactor their original project. However, Ryan Dahl wanted to implement some radical changes that just couldn’t be done with Node.js because it would break compatibility.

A total rewrite is obviously a huge risk, but Deno crossed the chasm and has great momentum.

Also, remember that Deno is still very young; the first commit was on May 13th, 2018, and version 1.0 was released exactly two years later on May 13th, 2020.

Let’s see how Deno compares to Node.js

Deno vs. Node.js

Here are the main differences between Node.js and Deno. Later, we’ll discuss the internals too, but here we focus on the functional and developer experience differences.

built-in package management vs npm

Node.js relies on npm for package management. Deno on the other hand follows the example of Go and Rust and can import packages from anywhere by URL.

ES modules vs. CommonJS modules

Node uses the CommonJS standard:

const module = require(‘module-name’)

Deno uses the standard EcmaScript modules:

import module from ‘https://some-repo/module-name.ts'

Note that Deno requires fully qualified module names including the extension.

Permission-based access vs. full-access

Node gives you full access to the environment, file system, and network. This is a serious security vulnerability. There were several attacks where malicious npm modules took advantage of this and gained access to these resources.

Deno requires explicit permissions, so it limits the exposure to bad actors.

Built-in typescript compiler vs. external support

Node doesn’t support Typescript directly, You need to use a heavyweight and always changing toolchain with bundlers, transpilers, etc.

Deno has first-class support for Typescript, which makes it much more streamlined to work with.

Promises vs. callbacks

Node uses non-blocking I/O and requires callbacks to get notified when I/O operations are complete.

Deno instead uses the modern async/await paradigm, which hides the complexities involved with callback chains and makes the code much cleaner and easier to reason about.

Die on error vs. uncaught exceptions

In Node you can have a global handler for all uncaught exceptions:

process.on(‘uncaughtException’, function (err) {

console.log(‘ignoring…’);

})

In Deno, your program will die if you don’t catch an exception. This a major design decision.

Let’s take a closer look at Deno’s primary features.

Modules and package management

With Deno you import modules by URL. There is no need for package.json and node_modules. That said, there is a cache, so you download packages and modules just once. Here is an example:

If your arithmetic skills are on point, you’ll notice that the assertion is incorrect (2 + 2 is actually 4). Let’s see if Deno thinks so as well:

$ deno run ./assert.ts

error: Uncaught AssertionError: Values are not equal:

[Diff] Actual / Expected

- 4

+ 5

throw new AssertionError(message);

^

at assertEquals (https://deno.land/std@0.93.0/testing/asserts.ts:219:9)

at file:///Users/gigi.sayfan/git/deno_test/assert.ts:3:1

Yep. Let’s fix it.

Now, it succeeds:

$ deno run ./assert.ts

Check file:///Users/gigi.sayfan/git/deno_test/assert.ts

success!

OK, back to packages and imports. Consider this line:

import { assertEquals } from “https://deno.land/std@0.93.0/testing/asserts.ts";

Here Deno imports the assertEquals symbol (which happens to be a function in the Deno standard library) directly from a URL. Note that the URL contains version information (std@0.93.0), so it’s easy to support multiple versions of the same package.

Deno maintains a curated list of packages in https://deno.land, but you can import packages from any URL.

To demonstrate this try replacing the import statement with the following equivalent import statement that imports using the Github URL that hosts Deno’s standard library:

import { assertEquals } from “https://raw.githubusercontent.com/denoland/deno_std/main/testing/asserts.ts"

Async support

Deno returns promises from its asynchronous APIs. This means that you can run your asynchronous operation and wait for the result without orchestrating a spaghetti ball of callback functions.

Here is a crazy example:

What’s going on here? We use the Deno.run() API to launch a sub-process. The subprocess we launch is another instance of Deno and we evaluate the expression console.log(2+3), which of course prints 5 to the console.

In this case, the sub-process returns almost immediately, but for long-running processes, we want to wait for completion without blocking the rest of our problem, which is why we await the promise.status().

Deno and Typescript

Deno has built-in support for Typescript. That means that unlike Node or web applications running in the browser there is no need for heavyweight and non-standard toolchain. Deno contains a Typescript compiler and it will transpile your Typescript code to JavaScript and execute it later on the V8 runtime.

Deno caches transpiled Typescript modules, so as long as the Typescript file doesn’t change it will not be transpiled again.

To see the cache location check the emitted modules cache:

$ deno info

DENO_DIR location: “/Users/gigi.sayfan/Library/Caches/deno”

Remote modules cache: “/Users/gigi.sayfan/Library/Caches/deno/deps”

Emitted modules cache: “/Users/gigi.sayfan/Library/Caches/deno/gen”

Language server registries cache: “/Users/gigi.sayfan/Library/Caches/deno/registries”

Security

One of the primary reasons for creating Deno is security. Deno was designed to be secure from the ground up and users control what level of access each program has.

Resources like the network, the environment, and the file system are inaccessible by default. For example, let’s run a program that tries to write a file and see what we get.

Let’s save the following snippet to a file called write_file.ts.

Then try to run it:

$ deno run write_file.ts

Check file:///Users/gigi.sayfan/git/deno_test/write_file.ts

error: Uncaught PermissionDenied: Requires write access to “1.txt”, run again with the — allow-write flag

Deno.writeTextFileSync(‘data.txt’, ‘some data’)

^

at unwrapOpResult (deno:core/core.js:100:13)

at Object.opSync (deno:core/core.js:114:12)

at openSync (deno:runtime/js/40_files.js:32:22)

at writeFileSync (deno:runtime/js/40_write_file.js:24:18)

at Object.writeTextFileSync (deno:runtime/js/40_write_file.js:82:12)

at file:///Users/gigi.sayfan/git/deno_test/write_file.ts:1:6

As expected we got a permission error, with a useful message telling us what flag we need to add. Let’s run the program again with the proper permission flag:

$ deno run — allow-write write_file.ts

Check file:///Users/gigi.sayfan/git/deno_test/write_file.ts

$ cat data.txt

some data

Note that the Deno REPL has all the permissions. So, be careful if you run untrusted code in an interactive Deno session.

Tooling

Deno puts a lot of emphasis on the developer experience. It provides a lot of tools out of the box. I like this batteries-included approach. Let’s review quickly the various tools Deno provides.

Formatting

Formatting is arguably not essential and is just nice to have. But, the developer community learned over time that a lot of energy was poured over white space and brace placement arguments and flame wars.

Deno takes a page from Go and Rust and provides a `deno fmt` command. Let’s see what official deno formatting looks like.

Consider the following file fmt-test.ts:

It is a valid Typescript program. But, it’s not formatted very well.

Let’s run it through deno fmt:

$ cat fmt_test.ts | deno fmt -

Here is the result:

Deno fmt put the opening brace of the function on the same line as the function declaration, indented everything with two spaces, converted single quotes to double quotes, put spaces around the “+” operator, and added semicolons at the end of each line.

Testing

Testing is an essential part of programming. Deno doesn’t just rely on the community to come up with test frameworks and comes geared up with its own assertions module you can use to write tests. Let’s give it a try.

In the test-test.ts file I define a function called is_palindrome() that checks if a string is a palindrome or not (ignoring spaces) and then a few tests, The first two tests should pass and the third one should fail:

To run the test I use the deno test command:

$ deno test test_test.ts

Check file:///Users/gigi.sayfan/git/deno_test/$deno$test.ts

running 3 tests

test Palindrome 1 — success … ok (1ms)

test Palindrome 2 — success … ok (1ms)

test Palindrome 3 — fail … FAILED (2ms)

failures:

Palindrome 3 — fail

AssertionError: fail!

at assert (https://deno.land/std@0.95.0/testing/asserts.ts:178:11)

at file:///Users/gigi.sayfan/git/deno_test/test_test.ts:19:3

at asyncOpSanitizer (deno:runtime/js/40_testing.js:37:15)

at resourceSanitizer (deno:runtime/js/40_testing.js:73:13)

at Object.exitSanitizer [as fn] (deno:runtime/js/40_testing.js:100:15)

at TestRunner.[Symbol.asyncIterator] (deno:runtime/js/40_testing.js:272:24)

at AsyncGenerator.next (<anonymous>)

at Object.runTests (deno:runtime/js/40_testing.js:347:22)

at async file:///Users/gigi.sayfan/git/deno_test/$deno$test.ts:3:1

failures:

Palindrome 3 — fail

test result: FAILED. 2 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out (4ms)

I’ll leave fixing the test to you as the dreaded exercise to the reader!

Bundling

Bundling lets you package all the modules and dependencies of your program into a single bundle that can be executed without delay. Deno provides the bundle command. Let’s see it in practice. The foobar.ts module imports the foo() function from foo.ts and the bar() function from bar.ts.

Here is foo.ts:

Here is bar.ts:

https://gist.github.com/the-gigi/2d48187fcc1e11ad6c27299487b1a9e0

Here is foobar.ts:

Let’s bundle them all up into a single file:

$ deno bundle foobar.ts

Bundle file:///Users/gigi.sayfan/git/deno_test/foobar.ts

Check file:///Users/gigi.sayfan/git/deno_test/foobar.ts

Here is the result:

As you can see the import statements are no longer needed because foo() and bar() are all embedded in the single bundle file and can be invoked directly.

Debugging

Deno allows you to debug your programs via the V8 inspector protocol. You can use Chrome DevTools and run your program with the — inspect or — inspect-brk flags. I personally prefer JetBrains IDEs and the Deno plugin that lets me debug Deno code using native JetBrains debugging with full-fledged breakpoints, watches, and stack traces.

If your weapon of choice is VS Code then a Deno plugin is in progress. In the meantime you can attach the debugger manually using the following launch.json configuration file:

Script installation

Running deno programs using deno run is fine in many cases, but if you need to pass a lot of permission flags and you want to be able to run the program from any location there is a better choice. Deno provides the deno install command that creates a little shell script to invoke your Deno program and places it in a location for your designation or in $HOME/.deno/bin

Let’s install our foobar program:

$ deno install foobar.ts

✅ Successfully installed foobar

/Users/gigi.sayfan/.deno/bin/foobar

ℹ️ Add /Users/gigi.sayfan/.deno/bin to PATH

export PATH=”/Users/gigi.sayfan/.deno/bin:$PATH”

I added $HOME/.deno/bin to my PATH and now I can run foobar from anywhere just by typing foobar:

$ cd /tmp

$ foobar

foo

bar

Deno internals

We reviewed Deno's capabilities and the user experience. Let’s take a look under the hood. Deno is implemented using Rust and TypeScript. Here are the main components of Deno:

  • deno
  • deno_core
  • tsc
  • swc
  • rusty_v8

The deno crate is the deno executable, which you interact with.

The deno_core crate is responsible for the Javascript execution runtime. Deno core relies on the Tokio crate for implementing its async event loop.

The tsc tool is the standard TypeScript compiler. It used to be Deno’s TypeScript compiler. Now, it is mostly responsible for type checking.

The swc project stands for Speedy Web Compiler and it takes on more and more work to compile your Javascript and Typescript code to Javascript that can be executed on any browser.

Finally, the rustry_v8 crate provides Rust bindings to the V8 C++ API.

Conclusion

Deno is a young project with a lot of momentum. It is built on the experience and hard lessons of Node.js. It has a lot of technical improvements over Node.js. It is implemented using a modern tech stack. The big question is if it’s going to take over as the primary Javascript and Typescript backend runtime. I think it’s too early to tell, but it should be very interesting to watch what happens. If you have some server-side project you planned to implement using Node.js consider giving Deno a try.

As with everything else, choosing between Node.js, Deno, or some other technology should mostly depend on the needs of your project and the expertise of your team.

If you already have something built with Node.js that is working well, rewriting it to Deno without some other benefit might not be the most optimal decision.

For example, one of the backend services of SessionStack is built in Node.js.
The reason is that SessionStack’s library sends a wide variety of data from the browser such as DOM changes, user interactions, JavaScript exceptions, stack traces, network requests, and debug messages. This data is later used by SessionStack’s platform which allows teams to replay user journeys as videos in order to optimize product workflows, reproduce bugs, or see where users are stuck.
The service that is consuming the data from the browser is very heavy on I/O operations. This was a great use case for Node.js and Deno was not available back then.
However, we are excited to monitor how Deno evolves and potentially it can be included in our stack in the future.

There is a free trial if you’d like to give SessionStack a try.

SessionStack replaying a session

If you missed the previous chapters of the series, you can find them here:

--

--