exenv
exenv is a tiny CommonJS-era feature detector extracted from React's old private ExecutionEnvironment module. Requiring it gives you four booleans: whether a DOM, Worker constructor, event listener API, and screen-backed viewport were present when the file first ran. It does not identify browsers, detect Node.js directly, polyfill missing APIs, or update its answers after import. Its original purpose was keeping packages from reaching into React internals when deciding whether browser-only code was safe during server rendering.
Keep it when an older dependency graph already speaks its four-flag API. For new code, direct capability checks or a maintained environment classifier are easier to type, audit, and reason about.
Use it if
- You maintain an older CommonJS package that already expects React-style canUseDOM and related flags
- You need one dependency-free guard before touching window, document, addEventListener, or screen
- You must preserve behavior shared with old packages that depend on exenv rather than redesign their environment checks
- You only need a DOM check in new code: typeof window !== 'undefined' && window.document is clearer than adding an inactive package for one boolean
- You need TypeScript declarations or an ESM export: version 1.2.2 ships one UMD-style index.js file, no bundled types, and no module field
- Your globals can appear after startup, as in tests that install jsdom late: all four answers are computed once during module evaluation and then cached by the module loader
- You need accurate runtime classification: canUseWorkers only tests whether Worker exists, so it does not distinguish a browser window, Web Worker, Node worker_threads, Deno, or a test shim
- Maintenance status matters to your supply-chain policy: the latest npm release is from April 2017 and the repository's last push was in November 2018
Setup reality
Installation is only npm install exenv, and there are no runtime dependencies, peer dependencies, native builds, credentials, or config files. The catch is compatibility rather than installation. The package exposes CommonJS through a UMD wrapper, so modern ESM and TypeScript projects may need a default import interop setting, createRequire, or a local declaration instead of getting a clean named export. It bundles no type definitions. Its four booleans are snapshots created as soon as index.js executes. If a test imports exenv before installing jsdom globals, canUseDOM stays false even after window is added; reset the module cache or install the environment before importing. The reverse also applies if globals disappear. canUseWorkers is independent of canUseDOM and checks only the global Worker name, while event listeners and viewport require a usable DOM first. This is capability detection, not browser detection, and it should not be used to make security decisions. Browser script-tag loading creates window.ExecutionEnvironment, AMD loaders get define(), and Node gets module.exports, but the README documents only the require form. There is no build step or ongoing configuration to fix these limits because version 1.2.2 is effectively frozen.
Patterns
Guard browser-only DOM workguard-dom-access
const { canUseDOM } = require('exenv');
if (canUseDOM) {
const root = document.getElementById('app');
root?.classList.add('ready');
}The flag prevents direct document access during Node.js rendering, but it reflects the globals present when exenv was first required.
Choose a server-rendering fallbackchoose-ssr-fallback
const ExecutionEnvironment = require('exenv');
const viewportWidth = ExecutionEnvironment.canUseDOM
? window.innerWidth
: 1024;A false value means no usable DOM was detected; it does not prove that the runtime is Node.js or that 1024 is the real client width.
Create a Web Worker only when exposedcheck-workers
const { canUseWorkers } = require('exenv');
const worker = canUseWorkers
? new Worker('/search-worker.js')
: null;This only checks whether a global named Worker exists. It does not test construction, URL support, Content Security Policy, or Node worker_threads.
Attach a global listener conditionallycheck-event-listeners
const { canUseEventListeners } = require('exenv');
if (canUseEventListeners) {
window.addEventListener('resize', onResize);
}The source accepts either addEventListener or old attachEvent, but this snippet intentionally uses the modern API after the guard.
Read screen information safelycheck-viewport
const { canUseViewport } = require('exenv');
const screenSize = canUseViewport
? { width: window.screen.width, height: window.screen.height }
: null;canUseViewport means window.screen was truthy. It says nothing about visualViewport, layout viewport dimensions, or measurement accuracy.
Load a DOM-dependent module only in the browserdefer-browser-module
const { canUseDOM } = require('exenv');
const editor = canUseDOM
? require('./browser-editor')
: require('./editor-placeholder');The conditional must surround require itself. Importing the browser-only module at the top level can fail before canUseDOM is checked.
Set up jsdom before importing exenv in a testinstall-jsdom-first
const { JSDOM } = require('jsdom');
const dom = new JSDOM('<!doctype html>');
global.window = dom.window;
global.document = dom.window.document;
const { canUseDOM } = require('exenv');
console.assert(canUseDOM === true);Import order is part of the behavior because Node caches the computed export. Requiring exenv before these assignments leaves the value false.
Refresh the cached result between CommonJS testsrefresh-test-snapshot
const modulePath = require.resolve('exenv');
delete require.cache[modulePath];
const environment = require('exenv');
console.log(environment.canUseDOM);Clearing application module caches can have side effects. Prefer installing test globals before imports or isolating each environment in a separate process.
Load the CommonJS package from Node ESMuse-from-esm
import { createRequire } from 'node:module';
const require = createRequire(import.meta.url);
const { canUseDOM } = require('exenv');There is no native ESM entry or named-export declaration. Some bundlers synthesize imports, but createRequire is the explicit Node.js route.
Describe the export locally in TypeScriptnarrow-local-type
type ExecutionEnvironment = {
canUseDOM: boolean;
canUseWorkers: boolean;
canUseEventListeners: boolean;
canUseViewport: boolean;
};
const env = require('exenv') as ExecutionEnvironment;Version 1.2.2 does not bundle TypeScript declarations. Check whether your project already receives community declarations before adding a duplicate module declaration.
Alternatives
| Package | Registry | Pick it when |
|---|---|---|
| can-use-dom | npm | You want a single-purpose CommonJS DOM availability check with an even smaller API |
| browser-or-node | npm | You need to distinguish browser, Node.js, Web Worker, jsdom, and Deno environments |
| is-browser | npm | You only need a simple browser boolean and accept another small legacy package |