Learn how to build runtime-agnostic JavaScript apps and packages using modern runtimes like Node.js, Bun and Deno with reliable detection methods.
Node.js popularized the concept of running JavaScript on the server. Today, there are more JavaScript runtimes intended for building server-based applications, with Bun and LLRT among the most recent I’m aware of. Deno has been around for some years, and their Version 2 release seems to hit a chord because it has better compatibility with the Node.js API.
Some of these runtimes have adopted Web Platform APIs to allow the same API to be used both in the browser and the server (e.g., web crypto API)—however, they also have runtime-specific APIs.
For example, Deno has specific APIs like Deno.env
which behaves like process.env
in Node.js but with more methods to get, set or delete environment variables. Each platform has runtime-specific APIs that make certain tasks simpler to achieve. But how can you use a runtime-specific API and still allow your app/program to work across platforms (i.e., runtime-agnostic)?
I will show you the best way to detect the runtime/platform and what you can do with it. I’ll start with the best option, then wrap up with common suggestions that will likely fail.
The right way to know what runtime your code runs on is to use navigator.userAgent
. It returns a string containing the runtime and optionally the runtime’s version.
You might be familiar with this API from the browser spec as a way to determine the browser your code runs on. Although browser detection using this approach is not recommended, the WinterCG group has agreed on this as part of their minimum Common Web Platform API for Non-Browser ECMAScript-based runtimes.
Here’s an example of the result you get when you call that API in some of the JavaScript runtimes today:
Runtime | Output |
---|---|
Node.js | Node.js/22 |
Bun | Bun/1.0.28 |
Cloudflare Workers | Cloudflare-Workers |
Deno | Deno/1.40.0 |
This becomes useful when you’re building cross-platform apps or packages. I’ve been using Bun for some projects, and Bun implements a set of convenience functions for asynchronously consuming the body of a ReadableStream
and converting it to various binary formats. When you build a package that uses one of these APIs, you can use navigator.userAgent
to switch to the right implementation for the specific runtime. Doing something similar in a runtime like Node.js or Workers would require more code. Instead of showing that here, I’ll use a much simpler example.
Let’s see how to implement a function that logs the string representation of an object.
function logObject(obj) {
const runtime = navigator.userAgent;
if (runtime.startsWith("Deno")) {
console.log(Deno.inspect(obj));
} else if (runtime.startsWith("Bun")) {
console.log(Bun.inspect(obj));
} else {
//assume Node.js
const util = require("node:util");
console.log(util.inspect(obj));
}
}
class Foo {
get [Symbol.toStringTag]() {
return "bar";
}
[Symbol.for("Deno.customInspect")]() {
return "I'm running in Deno";
}
}
class Bar {}
const obj = {
a: 10,
b: "hello",
test: function () {},
};
console.log("Running in " + navigator.userAgent);
logObject(obj);
logObject(new Bar());
logObject(new Foo());
The logObject()
function checks for the runtime and then uses the runtime-specific API to retrieve a string representation of the object. The GIF below shows what you’ll get when you run the code.
If you look closely, you’ll notice that each runtime returns the value in a slightly different format, and with Deno we get a custom output when we specify a custom inspect function.
We’ve seen how to use navigator.userAgent
as the right way, but you’ll find different suggestions when you Google for answers. For example, this code was suggested on Stack Overflow as a way to check if it’s running on Deno:
if ("Deno" in window) {
console.log("window.Deno=", window.Deno);
} else {
console.log("no Deno here");
}
That’ll work, but you risk the chance of a false result if you or your dependencies attach an object with the same name to the window
object.
There are API-specific hacks that look like one of these:
function isNode() {
try {
const fs = await import('fs');
return true;
} catch () {
return false;
}
}
function isNode() {
try {
const env = process.env;
return true;
} catch () {
return false;
}
}
The problem with that approach is that now that Cloudflare supports process.env
, code that used to work reliably would no longer work. Recent Cloudflare Workers release also supports importing Node.js modules with the node:
prefix , which likely would make any app relying on that fail.
In this article, we explore how to determine the runtime environment for JavaScript code, especially with the advent of modern runtimes like Bun and Deno. The best approach is to use navigator.userAgent
which provides a string identifying the runtime and optionally its version. The WinterCG group endorses this method as part of their minimum Common Web Platform API.
We also discussed less reliable methods, such as checking for specific global objects or APIs, which can lead to inaccuracies due to overlapping capabilities among different runtimes.
Peter is a software consultant, technical trainer and OSS contributor/maintainer with excellent interpersonal and motivational abilities to develop collaborative relationships among high-functioning teams. He focuses on cloud-native architectures, serverless, continuous deployment/delivery, and developer experience. You can follow him on Twitter.