Deno logoDeno

Deno 1.21 Release Notes


Deno 1.21 has been tagged and released with the following new features and changes:

If you already have Deno installed, you can upgrade to 1.21 by running:

deno upgrade

If you are installing Deno for the first time, you can use one of the methods listed below:

# Using Shell (macOS and Linux):
curl -fsSL https://deno.land/x/install/install.sh | sh

# Using PowerShell (Windows):
iwr https://deno.land/x/install/install.ps1 -useb | iex

# Using Homebrew (macOS):
brew install deno

# Using Scoop (Windows):
scoop install deno

# Using Chocolatey (Windows):
choco install deno

deno check, and the path to not type checking by default

The last few years have shown how tremendously useful it is to be able to annotate JavaScript code with type information (usually in the form of TypeScript). This has two primary benefits:

  • Type information helps generate documentation automatically, both in the form of IDE hints / completions, and in the form of static documentation pages like on https://doc.deno.land.
  • Type information can be used to verify that a given bit of code is semantically correct (type check).

Both of these benefits are very useful, but at different times. The first is an ambient benefit that is always useful during development. The second is a more sophisticated benefit that is only useful just before you are going to ship some code. The type checker is really just a very very powerful linter that can help you discover problems in your code before you run it.

Up to now, deno run has always automatically performed type checking on the code it was about to run, just before running it. This can sometimes be a nice user experience, but more often than not it is not what you want. The reason for this is often that type checking is really slow: it is often by far the single largest factor impacting the startup performance of your application.

The thing is that most developers use an IDE that surfaces the results of the type check at development time already. All users using deno lsp get this experience out of the box. This means that when they run their freshly developed code, they have to wait a long time for a type check to be performed even though it is not actually useful, because they can already see all diagnostics in their IDE.

Additionally, with JavaScript being on the path to get type annotations natively, the semantics browsers will use when they encounter type comments will be unlike Deno. They will not type check before running some code, instead leaving that step to a separate type checking step that developers run before shipping the code.

In line with this, we have made the decision to start Deno on the path of disabling type checking by default in deno run. Type checking will need to be performed explicitly using the new deno check subcommand. Because we know that this change is rather invasive at this time, we are going to take it slow. Our tentative plan for the timeline to make this change is as follows:

  • This release adds the new deno check subcommand, and a DENO_FUTURE_CHECK=1 environment variable that can be set to switch Deno into the "new" no-typecheck-by-default mode that will be default in the future.
  • We will spend a couple of releases doing outreach and informing users of the change.
  • In an upcoming release in a couple of months, we will make the switch to disable type checking by default on deno run.

Disabling type checking by default on deno run does not mean we are removing TypeScript support from Deno. TypeScript is still a first class language in Deno, and we will continue to encourage users to use TypeScript for their projects. The only change that we are making is that users will now have to explicitly specify when they want to perform type checking: either by running deno check, or by specifying the --check option on deno run.

The new deno check subcommand also has slightly different default type checking semantics compared to the existing type checking in deno run: it only reports diagnostics for the current project (like deno lint or deno fmt) and not any diagnostics for remote dependencies. This behaviour is already available in deno run through the --no-check=remote flag. Type checking of the entire module graph is still possible by running deno check --remote.

We know this change may have been unexpected by some users, but in reality we have been wanting to make this change for nearly a year. We think this change significantly improves developer experience for users of Deno, and we think that we can align ourselves better to what the JavaScript community as a whole has done in the past, and will do in the future.

If you have feedback on this change, or the planned timeline, please hop onto our Discord server to discuss.

globalThis.reportError and the "error" event

This release aligns Deno's error handling behaviour for uncaught exceptions in asynchronous event loop tasks like setTimeout, setInterval, or event handlers to the browser. Deno now has a global "error" event that will be dispatched for any uncaught exceptions in the above mentioned APIs. Users can event.preventDefault() this event to prevent the runtime from exiting with a non 0 status code like it usually would on uncaught exceptions.

Additionally, the web standard globalThis.reportError has been added to let users report errors in the same way that uncaught exceptions in asynchronous event loop tasks would. globalThis.reportError(error) is different to setTimeout(() => { throw error }, 0) in that the former synchronously performs the exception reporting steps, while the latter performs them in a future event loop tick (asynchronously).

Here is an example of the new feature:

// This code will cause Deno to print
// the exception to stderr and will exit with a non 0 status code.
reportError(new Error("something went wrong!"));
// But if a user handles the "error" event, they can prevent termination and log
// the error themselves:
window.onerror = (e) => {
  e.preventDefault();
  console.error("We have trapped an uncaught exception:", e);
};

reportError(new Error("something went wrong!"));

Thank you Nayeem Rahman for contributing this feature!

Improvements to the Deno language server and VSCode extension

Autodiscovery of config file and deno task integration

Deno's VSCode extension will now prompt you to enable it in a workspace if deno.json or deno.jsonc files are discovered.

Additionally, tasks defined in the tasks section from the config file will be available in the command palette:

Enable extension in subpaths of the workspace

This feature adds a long awaited ability to enable Deno extension only in some parts of the configured workspace by using Deno: Enable Paths or "deno.enablePaths" settings.

For example if you have a project like this:

project
├── worker
└── front_end

Where you only want to enabled the worker path (and its subpaths) to be Deno enabled, you will want to add ./worker to the list of Deno: Enable Paths in the configuration.

Testing API integration

vscode_deno now provides integration with VSCode's Testing API, making it possible to run your Deno test from the UI:

Make sure to update to latest version of Deno VSCode extension to be able to use these features.

Improvements to the REPL

The REPL is a tool for quick prototyping and trying new things and there's little use to perform type-checking, especially for imported, third party code. To this end, we decided to disable type-checking for imported modules in the REPL, leading to faster imports.

A new feature in this release, is the --eval-file flag that can be used with the deno repl subcommand. This flag allows you to pass a list of paths or URLs to files, that will be executed before the REPL starts. This feature is useful for creating custom, specialized REPLs.

$ deno repl --eval-file=https://deno.land/std@0.136.0/encoding/ascii85.ts
Download https://deno.land/std@0.136.0/encoding/ascii85.ts
Deno 1.21.0
exit using ctrl+d or close()
> rfc1924 // local (not exported) variable defined in ascii85.ts
"0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz!#$%&()*+-;<=>?@^_`{|}~"

Keep in mind, that files provided via the --eval-file flag, are executed in the same scope as the REPL itself. That means that all files are executed as "plain, old scripts"—not ES modules—and they all share the same global scope. One consequence of this, is that evaluated files may need to use absolute specifiers in import statements, as relative specifiers will be resolved relative to the current working directory of the REPL.

See more details in the manual entry

Thank you Naju Mancheril for contributing this feature!

In addition, the REPL now has a global clear() function available that acts as an alias for console.clear(). This aligns with what's found in the REPL of many browsers.

DENO_NO_PROMPT environmental variable

This release adds a new DENO_NO_PROMPT environment variable. When it is set, deno will disable all interactive prompts, even when the output is an interactive terminal. It has an identical effect to if one would specify --no-prompt on all invocations to the deno binary.

Improvements to unstable APIs

Unix socket support for Deno.upgradeHttp

The unstable Deno.upgradeHttp API that can be used to perform HTTP protocol switches now supports protocol switches on HTTP servers running on top of unix connections.

.unref() for Deno.Listener

The Deno.Listener API now has new .ref() and .unref() methods that can be called to enable or disable operations on this listener from blocking the event loop. For example, if a user creates a new listener by calling Deno.listen(), and then calls .accept() on the listener, the process will not exit until a connection has been accepted.

If the user calls .unref() on the listener after creation, the .accept() task would no longer block the process from exiting. The event loop would complete once all other asynchronous tasks have been completed, ignoring the completion state of the .accept() task of the listener.

Incremental formatting and linting

deno fmt and deno lint now use a behind the scenes cache in order to skip files it already knows are formatted or linted from previous runs. These subcommands are already very fast, but with this change you should see a noticeable performance improvement after you've run them at least once on some files, especially when working on a slow computer with little parallelism.

For example, here is how long deno fmt and deno lint took to run on deno_std's repo on a computer with 4 cores (2.60GHz) before this change:

$ time deno fmt
Checked 927 files
1.796s
$ time deno fmt
Checked 927 files
1.799s
$ time deno lint
Checked 872 files
2.201s
$ time deno lint
Checked 872 files
2.209

Now after:

$ time deno fmt
Checked 927 files
1.764s
$ time deno fmt
Checked 927 files
0.114s
$ time deno lint
Checked 872 files
2.292s
$ time deno lint
Checked 872 files
0.106s

Notice that the time goes down significantly after the first run of each command.

Additionally, starting with this release, deno fmt will automatically skip formatting files inside .git directories.

Improvements to deno bench

In Deno v1.20, we introduced a new deno bench subcommand, that allows to quickly register and run bits of code to assess their performance. This subcommand was modelled after deno test and had a similar output format.

We received great feedback about this feature from the community. Two complaints were especially vocal:

  • default number of iterations was too low in most cases
  • report format had too little information

Given that we marked Deno.bench() as an unstable API, we took the opportunity to address both complaints and add more features to the Deno.bench() API.

Deno.BenchDefinition.n and Deno.BenchDefinition.warmup that specified how many times each case should be run are now removed - instead the benchmarking tool will run a bench case repeatedly until the time difference between subsequent runs is statistically insignificant (this is similar to Golangs approach).

Deno.BenchDefinition.group and Deno.BenchDefinition.baseline were added, these fields allow you to neatly group related bench cases and mark one of them as the basis for comparison for other cases.

// This is the baseline case. All other cases belonging to the same "url"
// group will be compared against it.
Deno.bench({ name: "Parse URL", group: "url", baseline: true }, () => {
  new URL(import.meta.url);
});

Deno.bench({ name: "Resolve URL", group: "url" }, () => {
  new URL("./foo.js", import.meta.url);
});

Lastly, the bench report was reworked to include more useful information, including a comparison between cases from the same groups.

`deno bench` report

Thank you @evanwashere for contributing this feature.

New unstable API for subprocesses

Deno 1.21 adds new unstable subprocess APIs to the Deno namespace that incorporates much of the feedback we have received from the community about the (soon-to-be deprecated) Deno.run subprocess API.

High level API

A new easy-to-use API has been added to spawn a subprocess and collect its output in a single call. The Deno.spawn API takes an options bag similar to the options Deno.run takes, but instead of returning a Deno.Process object, it returns a Promise that resolves to a Deno.SpawnOutput. This contains the exit status of the process, and a Uint8Arrays containing the bytes the process outputted to stdout and stderr.

const { status, stdout, stderr } = await Deno.spawn(Deno.execPath(), {
  args: [
    "eval",
    "console.log('hello'); console.error('world')",
  ],
});
console.assert(status.code === 0);
console.assert("hello\n" === new TextDecoder().decode(stdout));
console.assert("world\n" === new TextDecoder().decode(stderr));

Low-level API

A new low-level API has also been added (Deno.spawnChild). It works very similar to Deno.run but has some notable differences:

  • All stdio streams are now ReadableStream / WritableStream instead of Deno.Reader / Deno.Writer.

  • Instead of calling proc.status() to get a promise resolving to the exit status of the process, you can now get the status promise from the child.status getter.

  • The subprocess no longer has to be manually closed with .close(), like in Deno.run.

  • A new child.output() API has been added that has the same return value as Deno.spawn().

const child = Deno.spawnChild(Deno.execPath(), {
  args: [
    "eval",
    "console.log('Hello World')",
  ],
  stdin: "piped",
});

// open a file and pipe the subprocess output to it.
child.stdout.pipeTo(Deno.openSync("output").writable);

// manually close stdin
child.stdin.close();
const status = await child.status;

Synchronous subprocess execution

Synchronous subprocess execution is a new capability that was not previously possible with Deno. This new Deno.spawnSync API has a signature that is nearly identical to Deno.spawn. The only difference is that the return type is SpawnOutput rather than Promise<SpawnOutput>.

const { status, stdout, stderr } = Deno.spawnSync(Deno.execPath(), {
  args: [
    "eval",
    "console.log('hello'); console.error('world')",
  ],
});
console.assert(status.code === 0);
console.assert("hello\n" === new TextDecoder().decode(stdout));
console.assert("world\n" === new TextDecoder().decode(stderr));

The new API is unstable. We encourage you to try it out though, and report any feedback you have.

Improvements to deno test

This release brings a lot of improvements and new features to Deno's built-in testing functionalities.

User code output formatting

Previously, deno test was not really aware of any output that might come from user code. That situation often led to interleaved output between test runner's report and console logs coming from your code. In this release we've reworked a lot of plumbing in the test runner, which allows us to be mindful of any output coming from your code, either from console API methods, or direct writes to Deno.stdout and Deno.stderr.

Starting in this release, if there is output coming from your code, it will be neatly enclosed between markers to make it distinct from the test runner's report.

`deno test` output

We are also considering capturing this output by default and only showing it, if the test fails. We'd love to hear your feedback, please let us know in the issue.

More informative errors and stack traces

deno test always provided full stack traces for thrown errors, however this is not desirable if your test files don't have a deep call stack. The result is that you'd see a lot of stack frames coming from internal Deno code, that don't help you pinpoint where something goes wrong:

// test.ts
Deno.test("error in a test", () => {
  throw new Error("boom!");
});
$ deno test test.ts
Check file:///dev/deno/test.ts
running 1 test from file:///dev/deno/test.ts
test error in a test ... FAILED (3ms)

failures:

error in a test
Error: boom!
    at file:///dev/deno/test.ts:2:9
    at testStepSanitizer (deno:runtime/js/40_testing.js:444:13)
    at asyncOpSanitizer (deno:runtime/js/40_testing.js:145:15)
    at resourceSanitizer (deno:runtime/js/40_testing.js:370:13)
    at Object.exitSanitizer [as fn] (deno:runtime/js/40_testing.js:427:15)
    at runTest (deno:runtime/js/40_testing.js:788:18)
    at Object.runTests (deno:runtime/js/40_testing.js:986:28)
    at [deno:cli/tools/test.rs:512:6]:1:21

failures:

    error in a test

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

error: Test failed

As can be seen above, the error contains 8 stack traces, but only the top frame contains useful information to debug the problem.

Starting in this release, deno test will filter out stack frames that are coming from Deno's internal code and show the line of code where the error originates:

$ deno test test.ts
Check file:///dev/deno/test.ts
running 1 test from ./test.ts
error in a test ... FAILED (5ms)

failures:

./test.ts > error in a test
Error: boom!
  throw new Error("boom!");
        ^
    at file:///dev/deno/test.ts:2:9

failures:

    ./test.ts
    error in a test

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

error: Test failed

Thank you Nayeem Rahman for contributing this feature!

We intend to further improve the test reporter in the coming releases to provide the best testing experience.

BDD style testing

We added a BDD (or Behavior Driven Development) style test runner to Deno Standard Modules in this release.

There are 2 major styles of organizing test cases in the JavaScript testing ecosystem. One is the test function style (tap, tape, and ava belong to this group), and the other is BDD (jasmine, mocha, jest, and vitest belong to this group) style (or describe-and-it function style).

While we chose a test function style for our default testing API (Deno.test()) for its simplicity, there has always been a need from the community for the BDD style syntax. Due to this need, we decided to add support for this style in this release, and the feature is now available in std/testing/bdd.ts.

The basic usage of describe and it functions looks like the following:

import {
  assertEquals,
  assertStrictEquals,
  assertThrows,
} from "https://deno.land/std@0.136.0/testing/asserts.ts";
import {
  afterEach,
  beforeEach,
  describe,
  it,
} from "https://deno.land/std@0.136.0/testing/bdd.ts";
import { User } from "https://deno.land/std@0.136.0/testing/bdd_examples/user.ts";

describe("User", () => {
  it("constructor", () => {
    const user = new User("John");
    assertEquals(user.name, "John");
    assertStrictEquals(User.users.get("John"), user);
    User.users.clear();
  });

  describe("age", () => {
    let user: User;

    beforeEach(() => {
      user = new User("John");
    });

    afterEach(() => {
      User.users.clear();
    });

    it("getAge", function () {
      assertThrows(() => user.getAge(), Error, "Age unknown");
      user.age = 18;
      assertEquals(user.getAge(), 18);
    });

    it("setAge", function () {
      user.setAge(18);
      assertEquals(user.getAge(), 18);
    });
  });
});

describe and it functions wrap the Deno.test API internally, so you can execute the above test with the deno test command as usual.

$ deno test bdd_example.ts
running 1 test from ./bdd.ts
User ...
  constructor ... ok (4ms)
  age ...
    getAge ... ok (3ms)
    setAge ... ok (3ms)
  ok (10ms)
ok (18ms)

test result: ok. 1 passed (4 steps); 0 failed; 0 ignored; 0 measured; 0 filtered out (40ms)

In addition to describe and it, we currently support 4 hooks beforeAll, afterAll, beforeEach, and afterEach, and also describe.only, it.only, describe.ignore, and it.ignore shorthands. Please see this document for more details.

Thank you Kyle June for contributing this feature!

Mocking utilities

We added mocking utilities to Deno Standard Modules in this release.

When you're building software with the external dependencies (such as Twitter's API, some banking API, etc.) and you can't control that external system, then testing such software often get really difficult. One way to solve that situation is to use a Mock object which simulates the behavior of the external systems. We now support that way of testing with our mocking utilities.

We added 2 basic mock types: spy and stub with assertion functions dedicated to these 2 types.

spy is a type of object that records every interaction with it and you can assert an interaction occurred later. The following example illustrates the basics.

import {
  assertSpyCall,
  assertSpyCalls,
  spy,
} from "https://deno.land/std@0.136.0/testing/mock.ts";
import { assertEquals } from "https://deno.land/std@0.136.0/testing/asserts.ts";

Deno.test("how spy works", () => {
  const func = spy();
  // The spy isn't called yet
  assertSpyCalls(func, 0);

  assertEquals(func(), undefined);
  // The spy was called with empty args
  assertSpyCall(func, 0, { args: [] });
  assertSpyCalls(func, 1);

  assertEquals(func("x"), undefined);
  // The spy was called with "x"
  assertSpyCall(func, 1, { args: ["x"] });
  assertSpyCalls(func, 2);
});

stub is the type of object which simulates some predefined behavior. The following example illustrates the basics.

import {
  returnsNext,
  stub,
} from "https://deno.land/std@0.136.0/testing/mock.ts";
import { assertEquals } from "https://deno.land/std@0.136.0/testing/asserts.ts";

Deno.test("how stub works", () => {
  // Replace Math.random with stub
  // now it returns 0.1 for the 1st call, 0.2 for the 2nd, 0.3 for the 3rd.
  const mathStub = stub(Math, "random", returnsNext([0.1, 0.2, 0.3]));
  try {
    assertEquals(Math.random(), 0.1);
    assertEquals(Math.random(), 0.2);
    assertEquals(Math.random(), 0.3);
  } finally {
    // You need to call .restore() for restoring the original
    // behavior of `Math.random`
    mathStub.restore();
  }
});

We don't list any realistic examples here as they require lot of space, but please see the document for more details and also examples.

Thank you Kyle June for contributing this feature!

Snapshot testing

We added snapshot testing tools in Deno Standard Modules in this release.

Snapshot testing is a powerful tool for testing software which produces complex output such as generated languages, ASTs, command line outputs, etc.

You can import assertSnapshot utility from testing/snapshot.ts which takes care for creating, updating, and validating the snapshots.

import { assertSnapshot } from "https://deno.land/std@0.136.0/testing/snapshot.ts";

Deno.test("The generated output matches the snapshot", async (t) => {
  const output = generateComplexStuff();
  await assertSnapshot(t, output);
});

The above call asserts that the output matches the saved snapshot. When you don't have snapshot yet or want to update the snapshot, you can update it by invoking deno test command with the --update option.

$ deno test --allow-read --allow-write -- --update
running 1 test from ./foo/test.ts
The generated output matches the snapshot ... ok (11ms)

 > 1 snapshots updated.

test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out (46ms)

Note that you need to give --allow-read and --allow-write permissions for the test process to read and write the snapshot files.

Thank you Yongwook Choi and Ben Heidemann for contributing this feature!

FakeTime testing utility

We added the FakeTime testing utility in Deno Standard Modules in this release.

Testing date time features is sometimes hard because those features often depend on the current system date time. In this release we added FakeTime utility in std/testing/time.ts that allows you to simulate the system date time and timer behaviors with it.

import { FakeTime } from "https://deno.land/std@0.136.0/testing/time.ts";
import { assertEquals } from "https://deno.land/std@0.136.0/testing/asserts.ts";

Deno.test("test the feature at 2021-12-31", () => {
  const time = new FakeTime("2021-12-31");
  try {
    // now points to the exact same point of time
    assertEquals(Date.now(), 1640908800000);
  } finally {
    time.restore();
  }
});

The FakeTime utility also simulates the behavior of timer functions such as setTimeout, setInterval, etc. You can control the exact elapsed time by using .tick() calls.

import { FakeTime } from "https://deno.land/std@0.136.0/testing/time.ts";
import { assertEquals } from "https://deno.land/std@0.136.0/testing/asserts.ts";

Deno.test("test the feature at 2021-12-31", () => {
  const time = new FakeTime("2021-12-31");
  try {
    let cnt = 0;
    // Starts the interval
    setInterval(() => cnt++, 1000);

    time.tick(500);
    assertEquals(cnt, 0);

    // Now 999ms after the start
    // the interval callback is still not called
    time.tick(499);
    assertEquals(cnt, 0);

    // Now 1000ms elapsed after the start
    time.tick(1);
    assertEquals(cnt, 1);

    // 3 sec later
    time.tick(3000);
    assertEquals(cnt, 4);

    // You can jump far into the future
    time.tick(997000);
    assertEquals(cnt, 1001);
  } finally {
    time.restore();
  }
});

Note that you always have to call time.restore() at the end of the test case to stop the simulation of the system date time.

Thank you Kyle June for contributing this feature!