Deno - An Unfinished Beauty
Tags: deno, node, javascript
Published: 2020-10-26 00:05:53

> # 2022 Update:
> My concerns around immaturity have long since been addressed since the 1.0 release. While issues around arm32 builds might still exist, my concerns around instability, ICU, npm-interop, and permissions have all been addressed. You may read this article as a lens into the time period when it was released as opposed to a modern look on the tooling.

This is going to be a bit dense. I've been using Deno for a while now and I've been told it might be useful to publish my thoughts on it so far. I'll be jumping around a lot, going over its features, so don't expect a super philosophically consistent article. This is mostly an opinion piece based on my experiences.

For those unaware, [Deno](https://deno.land/) is a JavaScript runtime. 

# A What?

The differences between a JavaScript runtime and an engine aren't super clear, but colloquially I've found it to be:

JavaScript Runtime = JavaScript Engine + User-Facing Layer

The user-facing layer would be both the included APIs and how you run your code. Chrome, Node.js, and Deno all use [Google's V8 engine](https://v8.dev/) to actually run the code, but they have varying APIs and ways with which you set up your code to be executed. 

With Chrome you must include the script in an executed HTML file, with Node.js you use the `node` binary and perhaps a `package.json`, and with Deno you use the `deno` binary with a bunch of flags to define security and features.

As for their APIs, some things stay the same, while others might differ. All platforms implement the [URL API](https://developer.mozilla.org/en-US/docs/Web/API/URL) the same. All platforms allow for [WebAssembly](https://developer.mozilla.org/en-US/docs/WebAssembly), but the way your run WASM in the web differs from Deno and Node. Node and Deno both include (almost identical) [path utilities](https://nodejs.org/api/path.html), whereas the web does not. Deno and the web often include very similar APIs such as [WebSockets](https://developer.mozilla.org/en-US/docs/Web/API/WebSocket), [FormData](https://developer.mozilla.org/en-US/docs/Web/API/FormData), and [ArrayBuffer](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/ArrayBuffer), whereas Node might have something vaguely similar or requires third-party libraries. The web also includes some very useful APIs such as [Canvas](https://developer.mozilla.org/en-US/docs/Web/API/Canvas_API) and [DOMParser](https://developer.mozilla.org/en-US/docs/Web/API/DOMParser) which Deno and Node are forced to use third-party libraries for.

But of course at the end of the day, they all run JavaScript. Deno is targeted at being a better version of Node, using the web and Node's faults as lessons to build from. It generally tries to match Web APIs where it can, but being a stand-alone binary, it can leverage a lot of power that a regular website cannot. 

# The Great

With that general introduction, Deno has a lot of really nice Quality Of Life features. I'm going to separate them into categories because they'd be too much to tackle individually.

## The Standard Library

This one isn't mentioned all that often, but Deno has a really nice [standard library](https://deno.land/std@0.74.0). It has a lot of helpful functions that really do feel like *extras* rather than *necessities*, but I don't mean that in a bad way. Where the web and node will be forced to use 3rd party libraries for many things, Deno has some great packages out of the box.

- [datetime](https://deno.land/std@0.74.0/datetime) - replacement for [moment](https://momentjs.com/) or [day.js](https://github.com/iamkun/dayjs)
- [encoding](https://deno.land/std@0.74.0/encoding) - replacement for hand-picked yaml, toml, and other parsers
- [log](https://deno.land/std@0.74.0/log) and [colors](https://deno.land/std@0.74.0/fmt/colors.ts) - replacement for [chalk](https://github.com/chalk/chalk)
- [testing](https://deno.land/std@0.74.0/testing) - replacement for [Jest](https://jestjs.io/) and [benchmark.js](https://benchmarkjs.com/)
- [uuid](https://deno.land/std@0.74.0/uuid) - replacement for [uuid](https://www.npmjs.com/package/uuid) (the npm package)

When I say "replacement" I don't mean they perform the same thing, but you might not need the entire 3rd party library when you can instead use Deno's versions.

And then of course you've got your standard filesystem utilities

- [path](https://deno.land/std@0.74.0/path) - like Node's path
- [fs](https://deno.land/std@0.74.0/fs) - like Node's fs
- [hash](https://deno.land/std@0.74.0/hash) - like Node's Crypto library
- [http](https://deno.land/std@0.74.0/http) - like Node's http

and so on.

A recurring theme in Deno is going to be removing the need for 3rd party tooling, a popular complaint of the modern JS ecosystem. I think this is a much nicer benefit than people give it credit for.

## TypeScript

I use TypeScript for just all my JavaScript projects these days. It's more about type completion and IDE intellisense than anything else. Refactoring code is much much easier and I don't have to keep tabbing between documentation and my code editor.

Deno includes TS compilation out of the box. It claims to be a runtime for TypeScript, but it's more like a compiler with some Deno integration. You can import a TypeScript file directly, it's rather quick at compiling, and it doesn't produce any JavaScript files as output, as it's unnecessary. 

Not needing to set up your whole project with ts-node is a big win for people upset with boilerplate and configuration.

It does feel like an unusual choice though, since TypeScript still feels "unofficial". It feels like as much of a necessity as something like Flow. I much prefer TypeScript and TypeScript does see much more use these days, but I do understand why some might see this as a bit controversial.

## Deno's API

This is not the same as its standard library as these don't need to be imported and are generally considered stable (more on that later). Here are 2 fantastic ones:

- [FormData](https://developer.mozilla.org/en-US/docs/Web/API/FormData) - a web API for sending forms instead of JSON-encoded data
    - Required for Discord Bots when uploading files, usually implemented with third party libraries in Node
- [fetch](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API) - a web API for sending server requests.
    - Node requires 3rd party libraries. There are several to choose from with different strengths.

I've done a fair amount of web testing and scraping with Deno. Having these 2 built in has been so much nicer than Node.js where the implementations are up to the library creators and may not follow how the web APIs work.

Deno also defaults to promise-based operations instead of callbacks. In Node you will often need to *[promisify](https://nodejs.org/dist/latest-v8.x/docs/api/util.html#util_util_promisify_original)* libraries or APIs in order to not slip into callback hell. With Deno, *[Iterables](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Iteration_protocols)* and promises are defaults, so there's no need for glue code anymore!

# The Good

Those are my favorites. Now onto the "good" - the things that are really nice to have, but not as important to me as the aforementioned materials.

## Deno's Multi-Purpose Binary

Deno's binary doesn't just run code. It also provides the ability to install Deno projects, inspect a Deno project, bundle code, generate documentation, and format.

- [installer](https://deno.land/manual@v1.4.6/tools/script_installer) - install Deno projects as CLI tools, like `npm i -g`
- [formatter](https://deno.land/manual@v1.4.6/tools/formatter) - format code like Prettier according to predefined rules
- [bundler](https://deno.land/manual@v1.4.6/tools/bundler) - collects all code into a single file for use, even inlines imports for use in web environments
- [doc generator](https://deno.land/manual/tools/documentation_generator) - emits documentation using JSDoc to stdout or JSON to be used with a documentation displayer.
- [info](https://deno.land/manual@v1.4.6/tools/dependency_inspector) - shows dependencies
- [watcher](https://deno.land/manual@v1.4.6/getting_started/command_line_interface#watch-mode) - mentioned in passing, it is an argument that arguably replaces [nodemon](https://www.npmjs.com/package/nodemon)

A lot of these were again possible with third-party tools, but having an opinionated, built-in way to do all this is super useful. The doc generator could be better, but is much better than nothing for all the people creating Deno libraries.

The formatter is opinionated, so it lets many Deno projects maintain a consistent style. There are some aspects to it that I would change if I could, but I'm sure others think similarly about parts I would hate to be changed, so it's a compromise.

The installer really cements my opinion of Deno's use case which I will again get into later.

## No Package.json

This can also be seen as a negative, but it's again one of the usual complaints against Node. The package.json file is often unwieldy and hard to read for some. It includes metadata about the package, custom scripts, and dependencies split into different categories. It does a lot at once, so Deno just gets rid of it. 

There is no one file to view all the dependencies, but that's what `deno info` is for. The less configuration a project has, often the easier it is to read, so that's a nice plus. With no package.json, package-lock.json, or node_modules/, the root directory is often a bit cleaner than most Node projects.

## Import Maps

One of the "bad" parts of Deno is that imports are always URLs or paths, never referencing some locally installed package directory, like `node_modules`. Deno caches imports, so this isn't extraordinarily problematic, but it does make imports a big ole mess.

We've gotten rid of both the package.json and package.lock files, but to remedy this, people usually end up making somewhat of an `exports.ts` file. This file imports everything from URLs and then re-exports it for the rest of the package to use. This feels like yet another package.json, so Deno also includes import maps.

[Import maps](https://deno.land/manual@v1.4.6/linking_to_external_code/import_maps) let you alias imports and are something seen usually only with build tools like [Rollup](https://rollupjs.org/guide/en/) and [Webpack](https://webpack.js.org/). So now instead of looking at:

```jsx
import { readText } from 'http://packages.example.org/deno/cool-coder55/CoolLib/src/utils';
```

We can instead have our imports looking like:

```jsx
import { readText } from 'CoolLib/src/utils';
```

I've taken advantage of this a lot. Sometimes URLs are also versioned, so this allows us to update the version of a library by changing it in one place instead of every URL in our project one-by-one.

## Security

It might seem a bit weird to list this one last, but it's not something I think is as effective as it seems.

Deno forces users to also list the [permissions](https://deno.land/manual@v1.4.6/getting_started/permissions) they grant a program when installing projects or running files. This can be things like network ability, write access, read access, and environment variable access. 

This is usually one of the first things about Deno mentioned and I do think it makes sense as a default. It's sort of like immutability as a default in a language, you'd rather choose times when things should have access then try to restrict things after. I just don't think it has quite the effect that it feels it would in practice.

The OSS scene is usually vetted for malicious code, so people are unlikely to check exactly why a program requires certain permissions. In fact, I often see people run files with `-A` which automatically grants all permissions, rendering this precaution null and void. However, I do see cases where this is useful.

# The Bad

With all the good over, we can now get into the frustrations. This section is not as large as the previous 2 combined, but it arguably has greater weight when deciding whether or not to adapt Deno for your projects. You may have been able to pick out some of this section from how I phrased things in earlier sections. 

## Immaturity

What's fairly obvious is Deno is new. like **really** new. v1.0.0 was released in May of this year (2020) and it still feels far from 1.0.0 when looked at as a whole. 

Deno still has [no Arm32 builds](https://github.com/denoland/deno/issues/2295) which means that hosting scripts on a Raspberry pi Zero, 1, or 2 is not yet possible. I unfortunately produced a Deno Discord bot using [Discordeno](https://github.com/Skillz4Killz/Discordeno) before finding this out and I have since rented a VPS. 

Deno has not had [ICU](https://en.wikipedia.org/wiki/International_Components_for_Unicode) support for a while, which means all JavaScript unicode-aware functions and properties will not work. This includes the `u` [RegExp](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/RegExp) flag and [String.prototype.normalize()](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/normalize). It was initially rejected due to the proportionally massive bump in file size this would force, but [recent discussion](https://github.com/denoland/deno/issues/1968) has shown that they're making progress with its integration.

There is clearly a problem with npm interop. There has been some recent work towards that goal, but it's nowhere near ready. There are ways to import web-libraries or node libraries that don't use any Node APIs, but if your png-to-jpeg Node library even makes a single call to Node's API, that package isn't getting in your Deno project without a repository fork.

There are still a few web APIs that Deno lacks. Node has some third party libraries to fill in the gaps, but these cannot be imported by Deno, so we're stuck without any option in that area. Sometimes the problem is even more fundamental than it just using Node APIs, like with [node-canvas](https://www.npmjs.com/package/canvas) using Cairo. I am watching [this issue](https://github.com/Automattic/node-canvas/issues/1653) at the moment, though.

I don't know of any build tooling, so if you have a particular feature you'd like to add onto JavaScript, like [glob imports](https://www.npmjs.com/package/import-glob), good luck. This is theoretically possible to implement, but I've yet to see it in practice and I think many users of Deno would be against it philosophically.

Deno has some support in code editors and debugging environments, but because it's a small player and it piggybacks off of the work done for Node.js, its debugger in VS Code **has** broke due to some update, which forced me to roll back my VS Code version.

Additionally, I've found bugs in Deno itself. This isn't much for a library, but when it affects crucial things like text rendering in console and leads to non-actionable error states, it surely presents a hurdle for anyone with less experience who wouldn't know where to turn.

In general, I don't think I would have faired so well if I didn't have years of experience with other frustrating dev environments and the know-how of who and where to ask for help.

## Breaking Changes

Oh, and on the topic of it not feeling like 1.0.0? The standard library seems to be far from 1.0.0 these days. 

The standard library of course being the thing containing many very important utility functions, like basic file reading and parsing. Really interesting new libraries get added often and this means the entire library's stability is held back by new content. Since, according to [the semver spec](https://semver.org/#spec-item-4), semantic versioning doesn't need to be respected before 1.0.0, the entire standard library is unstable. Since most Deno scripts use the standard library, you cannot guarantee that you're able to run an older Deno script without rolling back your Deno version. The version of Deno isn't listed anywhere in a project, so you just have to guess.

A lot of very important features are locked behind an `--unstable` flag. With Deno, unlike some other languages/projects, this often means breaking changes are more likely than not. An awful lot is still behind stable, so it's rare to find a project that doesn't require it if it ever interacts with the file system.

`--unstable` also doesn't let you opt into specific features. Recently, Deno forced projects to use `import type` when importing types or your code would not run. This also affects libraries you import, so you cannot use modern Deno features anymore with older libraries. This requirement is theoretically going to be pushed into stable Deno, breaking its stability (unless they push the version to 2.0.0 by then). 

The `tsConfig.json` **can** be edited, but most features cannot. Once you edit one, you must provide the defaults for everything else as well. Some features cannot be edited at all and this aspect wasn't extraordinarily clear, making the breaking change a very frustrating experience.

## The Lack of Scripts

With the removal of `package.json`, we can no longer bake a bunch of custom scripts into the project with short aliases. With Deno, the command to run a program can be rather long. Assuming the program has yet to be installed globally, you might be using:

```bash
 deno run --allow-net --allow-read=/usr --allow-write=/usr mod.ts -q Stephen Fry -n funny -r 10 -p 10 
```

And yes, this is very similar to a script from a project of mine. Everything after `mod.ts` is from my own project and so mostly my own doing. Luckily I can just press `UP` on my keyboard to re-execute it, but this makes managing multiple scripts a bit messy.

Some projects recommend including some sort of `run.sh` file when the same commands need to be run again fairly often. This ends up defeating the benefit of a cleaner root directory, but it does provide a simplification over `package.json` since the file only does one thing. This does require another file per script. We *could* probably add a `scripts.toml` file and then add a make a [nushell](https://www.nushell.sh/) or bash script to read it and execute the desired script, but these are again things not native to Deno.

Admittedly, the most common use cases for differing scripts are testing, transpiling TS, bundling, and file watchers. Deno includes all of those natively. One might use a bundler for more than what Deno provides, however, such as supporting custom JS features through a second stage of traspilation.  

# Conclusion - The Use Case

Deno is not production ready. It has passed 1.0.0, but it has not "passed 1.0". By "production ready" I mean don't build something if you're hoping for stability.

I'm a big fan of "new software" in general. Maybe that's a naive quality. I find that newer software has the advantage of creating much more sensible defaults based on past experiences. It is able to learn from other frustrations and break convention. Languages like Rust and Nim and now Deno show this well. 

Even Deno *libraries* are often "better" versions of Node libraries that had some clear faults. It often takes some getting used to, but you end up with a better product (either in DX or performance).

Deno's main use case seems to be with small scripts. As long as these scripts are bundled using `deno bundle` and a Deno compatibility version is listed, it should be fine. It will be rather frustrating to switch Deno versions each time you want to run a script, however.

I don't live in the CLI myself, but due to Deno's match with Web APIs, I've found web scraping and http requests to be much nicer with Deno. There should be some advancement on that end as well if Deno ever decides to add [DOMParser](https://github.com/b-fuze/deno-dom).

I'm going to continue using Deno for basic scripts and web scraping. I've worked through enough bugs and frustrations that I think I'll be able to avoid them to some extent. It's much easier to set up a Deno project than a Node.js one which makes prototyping much nicer. I think this is a use case most could get behind.