TL;DR
process.emitWarning() is the built-in Node.js approach and the right default for most libraries in 2026 — zero dependencies, standard output, integrates with --trace-warnings. depd is the battle-tested package that Express and many Connect-based middleware use — it deduplicates warnings (fires once per call site) and supports NODE_ENV=production silencing. deprecation is a lightweight alternative focused on typed warnings. For new libraries, start with process.emitWarning; use depd when you need per-call-site deduplication.
Key Takeaways
process.emitWarning(): Built-in Node.js API, no deps, fires every time (no deduplication)- depd: 45M+ weekly downloads, fires once per call site (deduplication), used by Express/Connect ecosystem
- deprecation: 8M+ weekly downloads, typed warnings, used by Octokit/GitHub libraries
- Deduplication is the key differentiator — depd only warns once per unique call site
- Node.js 22+:
--trace-deprecationand--throw-deprecationflags work with all three approaches - For Express middleware: depd is the standard (ecosystem convention)
The Problem
When you maintain a library, you need to signal that an API will change or be removed. Do it wrong and you either:
- Flood users' console with repeated warnings (every render, every request)
- Silently break their code in the next major version with no heads-up
Good deprecation warnings fire at the right time, give actionable information, and don't drown out other output.
process.emitWarning(): The Built-In Way
Node.js ships with a deprecation warning system. No npm install needed:
// Basic deprecation warning
function oldMethod(data: unknown) {
process.emitWarning(
"oldMethod() is deprecated. Use newMethod() instead.",
{
type: "DeprecationWarning",
code: "MY_LIB_DEP001",
}
);
return newMethod(data);
}
Output
(node:12345) [MY_LIB_DEP001] DeprecationWarning: oldMethod() is deprecated. Use newMethod() instead.
Runtime Flags
# Show stack traces for deprecation warnings
node --trace-deprecation app.js
# Turn deprecation warnings into thrown errors (useful in tests)
node --throw-deprecation app.js
# Silence all deprecation warnings
node --no-deprecation app.js
Programmatic Handling
// Users can catch deprecation warnings programmatically
process.on("warning", (warning) => {
if (warning.name === "DeprecationWarning" && warning.code === "MY_LIB_DEP001") {
// Log to monitoring, suppress output, etc.
}
});
The Problem: No Deduplication
function getUser(id: string) {
process.emitWarning("getUser() is deprecated. Use fetchUser().", {
type: "DeprecationWarning",
code: "DEP001",
});
return fetchUser(id);
}
// In a web server:
app.get("/users/:id", (req, res) => {
const user = getUser(req.params.id); // ⚠️ Warning fires EVERY request
});
// After 1000 requests: 1000 identical warnings in your logs
This is why depd exists.
depd: Fire Once Per Call Site
npm install depd # 1.4kB, zero dependencies
depd's core value: it tracks the call site (file + line number) and only emits the warning once per unique location:
import depd from "depd";
// Create a deprecation function scoped to your package
const deprecate = depd("my-library");
function getUser(id: string) {
deprecate("getUser() is deprecated. Use fetchUser() instead.");
return fetchUser(id);
}
// First call from app.js:15 → warning printed
// Second call from app.js:15 → SILENCED (same call site)
// First call from routes.js:42 → warning printed (new call site)
// Second call from routes.js:42 → SILENCED
Output Format
my-library deprecated getUser() is deprecated. Use fetchUser() instead. at app.js:15:3
The output includes the package name, the message, and the exact call site — users can find and fix the deprecation immediately.
depd for Properties
import depd from "depd";
const deprecate = depd("my-library");
const config = {
get oldProperty() {
deprecate.property(this, "oldProperty", "Use newProperty instead");
return this.newProperty;
},
newProperty: "value",
};
depd in Express Middleware
This is why depd has 45M+ weekly downloads — Express and its middleware ecosystem all use it:
import depd from "depd";
const deprecate = depd("my-middleware");
export function myMiddleware(options?: DeprecatedOptions) {
if (options?.legacyMode) {
deprecate("legacyMode option is deprecated. Use mode: 'compat' instead.");
}
return (req, res, next) => {
// middleware logic
next();
};
}
depd Environment Behavior
| NODE_ENV | Behavior |
|---|---|
development | Full warning with stack trace |
production | Short warning (no stack trace) |
test | Full warning |
| Unset | Full warning |
# Production: minimal output
NODE_ENV=production node app.js
# my-library deprecated getUser()
# Development: full call site info
NODE_ENV=development node app.js
# my-library deprecated getUser() is deprecated. Use fetchUser() instead.
# at Object.<anonymous> (app.js:15:3)
deprecation: Typed Warnings
npm install deprecation # 0.5kB, zero dependencies
The deprecation package (used by Octokit and GitHub's npm packages) creates typed Deprecation error objects:
import { Deprecation } from "deprecation";
function getUser(id: string) {
const warning = new Deprecation(
"[@my-org/my-lib] getUser() is deprecated. Use fetchUser() instead. " +
"See https://my-lib.dev/migration#getUser"
);
process.emitWarning(warning);
return fetchUser(id);
}
Output
(node:12345) DeprecationWarning: [@my-org/my-lib] getUser() is deprecated. Use fetchUser() instead. See https://my-lib.dev/migration#getUser
Why Use deprecation Over Raw emitWarning?
The Deprecation class extends Error, so it captures the stack trace automatically:
const warning = new Deprecation("message");
warning.name; // "Deprecation"
warning.message; // "message"
warning.stack; // full stack trace (captured at construction)
This integrates well with --trace-warnings and error monitoring tools (Sentry, Datadog) that recognize Error-like objects.
deprecation with Once Pattern
deprecation doesn't deduplicate by default. Combine with once:
import { Deprecation } from "deprecation";
import once from "once"; // or lodash.once
const warnGetUser = once(() => {
process.emitWarning(
new Deprecation("getUser() is deprecated. Use fetchUser().")
);
});
function getUser(id: string) {
warnGetUser(); // fires once, ever (not per call site)
return fetchUser(id);
}
Head-to-Head Comparison
| Feature | process.emitWarning | depd | deprecation |
|---|---|---|---|
| Zero dependencies | ✅ Built-in | ✅ Zero deps | ✅ Zero deps |
| Bundle size | 0kB | 1.4kB | 0.5kB |
| Deduplication | ❌ | ✅ Per call site | ❌ (DIY) |
| Stack trace | ✅ --trace-warnings | ✅ Automatic | ✅ Error-based |
| Production silencing | ❌ | ✅ NODE_ENV=production | ❌ |
| Warning codes | ✅ code: "DEP001" | ❌ | ❌ |
| Property deprecation | ❌ | ✅ .property() | ❌ |
| Typed/Error-like | ❌ (string) | ❌ (string) | ✅ extends Error |
| Express ecosystem | ⚠️ Not standard | ✅ Standard | ❌ |
| Weekly downloads | N/A | 45M+ | 8M+ |
Patterns for Your Library
The Simple Pattern (process.emitWarning)
Best for: small libraries, internal packages, one-off deprecations.
// utils/deprecation.ts
const warned = new Set<string>();
export function deprecate(code: string, message: string) {
if (warned.has(code)) return;
warned.add(code);
process.emitWarning(message, {
type: "DeprecationWarning",
code,
});
}
// usage
deprecate("MY_LIB_001", "foo() is deprecated. Use bar().");
The Express Pattern (depd)
Best for: middleware, large libraries, when call-site tracking matters.
import depd from "depd";
const deprecate = depd("my-package");
export function legacyHandler(req, res) {
deprecate("legacyHandler is deprecated, use modernHandler");
return modernHandler(req, res);
}
The GitHub/Octokit Pattern (deprecation)
Best for: API clients, SDKs, when you want Error-compatible warnings.
import { Deprecation } from "deprecation";
const warned = new Set<string>();
export function deprecate(message: string) {
if (warned.has(message)) return;
warned.add(message);
process.emitWarning(new Deprecation(`[my-sdk] ${message}`));
}
Testing Deprecation Warnings
import { describe, it, expect, vi } from "vitest";
describe("deprecation warnings", () => {
it("emits deprecation warning on first call", () => {
const warnSpy = vi.spyOn(process, "emitWarning");
oldFunction();
expect(warnSpy).toHaveBeenCalledWith(
expect.stringContaining("deprecated"),
expect.objectContaining({ type: "DeprecationWarning" })
);
warnSpy.mockRestore();
});
it("throws with --throw-deprecation behavior", () => {
// Simulate --throw-deprecation
process.on("warning", (warning) => {
if (warning.name === "DeprecationWarning") throw warning;
});
expect(() => oldFunction()).toThrow(/deprecated/);
});
});
Methodology
- Analyzed npm download data for depd and deprecation packages (March 2026)
- Reviewed Express, Connect, and Koa middleware source code for deprecation patterns
- Tested deduplication behavior of depd across Node.js 20, 22, and 23
- Compared output format and --trace-warnings integration across all three approaches
- Reviewed Octokit/GitHub SDK source code for deprecation package usage patterns
Integrating Deprecation Warnings with Monitoring and Observability Tools
Production observability platforms treat process.emitWarning output differently from unhandled errors, and knowing those differences affects how you design your deprecation strategy.
Node.js emits deprecation warnings to stderr by default. Most log aggregation pipelines (Datadog, Loki, CloudWatch) ingest stderr alongside stdout, so raw process.emitWarning calls will appear in your logs — but without structured metadata they are difficult to query, alert on, or correlate with affected users. Adding a code field (e.g. MY_LIB_DEP001) gives you a stable search key. You can query code:MY_LIB_DEP001 in Datadog or build a CloudWatch Insights query to count unique affected call sites per day.
The deprecation package's approach of extending Error integrates cleanly with Sentry and similar error monitors. When you call process.emitWarning(new Deprecation("...")), Sentry's Node.js SDK captures the warning event and records it with the full stack trace from the Deprecation instance. This gives you a Sentry issue per deprecated API method, with user impact counts, affected releases, and affected URLs — the same observability you get for actual errors. For library authors shipping public SDKs, this level of visibility into which deprecated APIs are still in use across customer codebases is invaluable for planning removal timelines.
depd's approach does not integrate with error monitors by default because depd intercepts the warning before it reaches the standard process.warning event in some environments. If you want depd warnings to flow into Sentry, listen to process.on('warning', ...) and forward warnings with code matching your library prefix.
Deprecation Strategies When Removing APIs Across Major Versions
The mechanics of emitting a warning are straightforward; the harder problem is designing a deprecation lifecycle that gives users enough time to migrate without keeping dead code in your library indefinitely.
The standard Node.js ecosystem convention is a two-major-version deprecation window: mark an API as deprecated in v2.0, remove it in v4.0. This means the deprecated API survives at least one major release where it is still present but warned, giving users a predictable timeline. Express follows this convention throughout its middleware ecosystem.
For the deprecation message itself, the most actionable pattern is: [what is deprecated] → [what to use instead] → [migration link]. A message like "getUser() is deprecated. Use fetchUser() instead. See https://my-lib.dev/migration#v3-getUser" is unambiguous. Users should never have to consult a changelog to understand what to do with a deprecation warning — the warning itself should contain the complete migration instruction.
When the replacement API requires a different call signature, include the new signature in the deprecation message. depd does not support multi-line messages, so keep the guidance concise but complete. The deprecation package's approach of including a URL is particularly valuable here — a migration guide can provide code examples and explain context that would be too verbose for a single-line warning message.
Automated detection of deprecated API usage is a growing practice. ESLint rules and @typescript-eslint/no-deprecated can flag deprecated symbols at the editor level, complementing runtime warnings. For library authors, publishing an ESLint plugin alongside your deprecations gives users a way to catch deprecated usage before runtime — shifting from reactive (see the warning in production) to proactive (blocked at lint time in CI).
Compare Express vs Fastify and other Node.js packages on PkgPulse — real-time npm download trends.
In 2026, library authors should use depd or process.emitWarning() rather than the deprecation package for new projects. depd gives you the cleanest API and the most control over when warnings fire, and it is the established pattern in the Express/Connect ecosystem. process.emitWarning() is the modern Node.js built-in approach — no extra dependency, integrates with Node.js' native warning system, and works everywhere Node.js runs. The deprecation package is useful when you need to track whether a deprecated code path has already warned (the de-dupe logic), but the same effect is achievable with depd's built-in de-duplication.
The discipline of writing clear, actionable deprecation messages pays dividends that are easy to underestimate. A warning that tells users exactly which API changed, what to use instead, and where to find migration documentation reduces support burden proportionally to how many consuming projects need to migrate. Libraries with cryptic deprecation messages drive confusion that generates issue reports and delayed migrations. Treating deprecation messages as user-facing documentation — with the same care given to API documentation — is one of the clearest signals of a mature, user-focused library maintenance practice.
See also: Motia: #1 Backend in JS Rising Stars 2025 and Best npm Packages for API Testing and Mocking in 2026, Best CLI Frameworks for Node.js in 2026.