npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@errio/node

v1.4.1

Published

Errio Node.js SDK — zero-config, import-only error instrumentation

Downloads

1,082

Readme

@errio/node

Zero-config error instrumentation for Node.js. Drop in one import and uncaught exceptions, unhandled rejections, framework errors, and structured logs all flow into Errio automatically.

Install

npm install @errio/node

Requires Node.js >= 18.

Quick start

import '@errio/node';

That's it. With ERRIO_API_KEY set in your environment, the SDK boots itself on import:

  • registers uncaughtException / unhandledRejection handlers
  • patches console.error / console.warn to capture them as breadcrumbs
  • registers all framework plugins (Express, Fastify, Anthropic SDK) and all logger plugins (Pino, Winston, Bunyan, Log4js, Roarr) so their middleware / transports / appenders / wrappers work the moment you wire them in

Environment variables

| Variable | Required | Description | |---|---|---| | ERRIO_API_KEY | yes | API key with ak_ prefix | | ERRIO_ENDPOINT | no | Base URL of your Errio API. Defaults to the public Errio endpoint. | | ERRIO_SERVICE_NAME | no | Service identifier (e.g. payment-api) | | ERRIO_ENVIRONMENT | no | Environment name. Defaults to NODE_ENV. | | ERRIO_SAMPLE_RATE | no | Float between 0.0 and 1.0. Defaults to 1.0. | | ERRIO_DEBUG | no | Set to true to log SDK activity to stdout — including the boot summary that lists each plugin's actual capture state. | | ERRIO_SILENT_UNCAUGHT | no | Set to true to suppress the SDK's stderr stack-trace print on uncaughtException. Default: prints [errio] uncaughtException: <stack> to stderr before flushing and exiting, so a crashing process still looks like a crashing process. | | ERRIO_LOGGER_CAPTURE_MIN_LEVEL | no | Minimum log level at which the logger plugins (winston, pino, bunyan, log4js, roarr, console) ALSO call captureError() when their args contain an Error. One of debug / info / warn / error. Default: error. Set to warn to also auto-capture warn-level Errors. | | ERRIO_ANTHROPIC_LATENCY_THRESHOLD_MS | no | Successful Anthropic messages.create calls slower than this number of milliseconds emit a model_timeout AI event. Default: 10000 (10 seconds). |

Important: load .env (e.g. with dotenv) before importing @errio/node. Auto-init reads process.env synchronously at import time — if env vars aren't set yet, the SDK silently no-ops.

Plugin reference

@errio/node ships nine auto-registered plugins. Some are active the moment you import '@errio/node'; others are dormant until you wire them into your framework or logger of choice. Active plugins capture errors. Pending plugins do nothing until you wire them.

| Plugin | Capture trigger | Activates by | |---|---|---| | console | ✅ Active on import | console.error(err) / console.warn(err) — captures the Error and adds a breadcrumb. Rate-limited to 10 captures/sec. | | express | ⚠️ Pending until wired | app.use(errorHandler()) — captures errors thrown by routes or next(err). | | fastify | ⚠️ Pending until wired | await app.register(fastifyErrioPlugin) — captures errors via the Fastify onError hook. | | winston | ⚠️ Pending until wired | new winston.createLogger({ transports: [winstonErrioTransport()] }) — captures Errors passed via logger.error('msg', err) or splat. | | pino | ⚠️ Pending until wired | pino(opts, pinoErrioTransport()) — captures Errors via the canonical logger.error({ err }, 'msg') shape. | | bunyan | ⚠️ Pending until wired | bunyan.createLogger({ streams: [{ type: 'raw', stream: bunyanErrioStream() }] }) — captures log.error({ err }, 'msg'). | | log4js | ⚠️ Pending until wired | log4js.configure({ appenders: { errio: { type: log4jsErrioAppender() } } }) — captures Errors found in log.error(...) args. | | roarr | ⚠️ Pending until wired | setupRoarrCapture() after import 'roarr' — captures log.error({ err, ... }, 'msg'). | | anthropic-sdk | ⚠️ Pending until wired | wrapAnthropicClient(new Anthropic()) — captures messages.create() and messages.stream() failures, ships latency telemetry to /api/v1/ai-events. |

By default, every logger plugin only calls captureError() when the log call's level is at or above error. Lower-level Error logging (e.g. logger.warn('something off', err)) still adds a breadcrumb but doesn't ship the Error. To widen the threshold, set ERRIO_LOGGER_CAPTURE_MIN_LEVEL=warn or pass loggerCaptureMinLevel: 'warn' to init().

Checking plugin state at runtime — getPluginStatus()

Drop this somewhere in your startup to catch "loaded but not wired" footguns before you ship:

import { getPluginStatus } from '@errio/node';

const pending = getPluginStatus().filter(
  (p) => p.state === 'pending' && p.name !== 'fastify' && p.name !== 'anthropic-sdk',
);
if (pending.length > 0) {
  console.warn(
    '[my-app] errio plugins not wired:',
    pending.map((p) => `${p.name} (${p.description})`).join(', '),
  );
}

getPluginStatus() returns one entry per registered plugin with name, state ('active' / 'pending' / 'loaded'), description, and an optional lastActivityAt timestamp. Bootstrapping with ERRIO_DEBUG=true also prints the same info as a single summary block at the end of init.

Manual init

If you need custom configuration or hooks, call init() explicitly. Don't set ERRIO_API_KEY in env in this case (otherwise auto-init fires first and init() becomes a no-op):

import { init } from '@errio/node';

init({
  apiKey: process.env.MY_ERRIO_KEY,
  endpoint: 'https://errio.example.com',
  environment: 'production',
  serviceName: 'payment-api',
  sampleRate: 0.5,
  debug: false,
  beforeSend: (event) => {
    // mutate or drop the event before it ships
    return event;
  },
});

Capturing errors

Errors are captured automatically from:

  • uncaughtException and unhandledRejection
  • Express via errorHandler() middleware
  • Fastify via fastifyErrioPlugin's onError hook

Or capture explicitly:

import { captureError } from '@errio/node';

try {
  riskyThing();
} catch (err) {
  captureError(err, {
    severity: 'high',
    tags: { route: '/checkout' },
    extra: { orderId: '...' },
  });
}

Express

import 'dotenv/config';
import '@errio/node';
import express from 'express';
import { requestHandler, errorHandler } from '@errio/node';

const app = express();

app.use(requestHandler()); // FIRST — wraps each request in an async context
app.get('/', (_req, res) => res.send('hi'));
app.use(errorHandler());   // LAST — captures thrown errors and ships them

requestHandler() must be the first middleware so the request's requestId, method, and route propagate into any error captured later. errorHandler() must be after all routes — it forwards via next(err), so add a terminal handler if you want a custom response.

Fastify

import 'dotenv/config';
import '@errio/node';
import Fastify from 'fastify';
import { fastifyErrioPlugin } from '@errio/node';

const app = Fastify();
await app.register(fastifyErrioPlugin);
app.get('/', async () => ({ ok: true }));
await app.listen({ port: 3000 });

fastifyErrioPlugin is wrapped with fastify-plugin so its onRequest and onError hooks apply to your entire app, not just to routes registered inside the plugin's own scope. You can register it before or after your routes.

Pino

import 'dotenv/config';
import '@errio/node';
import pino from 'pino';
import { pinoErrioTransport } from '@errio/node';

const log = pino(
  { level: 'debug' },
  pino.multistream([
    { level: 'debug', stream: process.stdout },
    { level: 'debug', stream: pinoErrioTransport() },
  ])
);

log.info({ userId: 'u_42' }, 'user signed in');
log.warn({ retry: 3 }, 'retrying upstream');

// 1.3.0+: Errors logged at level >= 'error' (configurable) ALSO ship to errio
// via the main capture pipeline — they land in the errors table next to
// everything else, with grouping/dedup/issue creation, not just as breadcrumbs.
log.error({ err: new Error('payment failed') }, 'charge declined');

Each pino log line is captured as a breadcrumb on the current request's context. The next error captured in that request will ship with the log lines attached. Pino's canonical Error shape is logger.error({ err }, 'msg') — by the time the transport sees the line, pino has serialized the Error into JSON, but @errio/node reconstructs a real Error (with the original stack) before capturing it.

Note: pino.multistream defaults each stream's level to info regardless of the root pino level. To capture debug lines too, set level: 'debug' on each stream entry explicitly (as shown above).

Winston

import 'dotenv/config';
import '@errio/node';
import winston from 'winston';
import { winstonErrioTransport } from '@errio/node';

const log = winston.createLogger({
  level: 'debug',
  format: winston.format.json(),
  transports: [
    new winston.transports.Console(),
    winstonErrioTransport(),
  ],
});

log.info('user signed in', { userId: 'u_42' });
log.warn('retrying upstream', { retry: 3 });

// 1.3.0+: any Error in the args (positional or via { err } / { error }) at
// level >= 'error' is auto-captured to errio in addition to the breadcrumb.
log.error('payment failed', new Error('card declined'));
log.error('lookup failed', { err: new Error('user not found') });

winstonErrioTransport() returns a winston-transport subclass instance. Each log call lands as a breadcrumb on the active request context. Winston levels map as: silly/debug/verbosedebug, http/infoinfo, warnwarn, error/crit/alert/emergerror. Unknown levels fall through to info.

The transport lazy-requires winston-transport only when called, so the SDK has no hard dependency on it — install winston yourself when you wire this in.

Bunyan

import 'dotenv/config';
import '@errio/node';
import bunyan from 'bunyan';
import { bunyanErrioStream } from '@errio/node';

const log = bunyan.createLogger({
  name: 'my-app',
  streams: [
    { stream: process.stdout },
    { type: 'raw', level: 'debug', stream: bunyanErrioStream() },
  ],
});

log.info({ userId: 'u_42' }, 'user signed in');
log.warn({ retry: 3 }, 'retrying upstream');

// 1.3.0+: Errors at log level >= 'error' are auto-captured to errio.
log.error({ err: new Error('payment failed') }, 'charge declined');

bunyanErrioStream() returns a type: 'raw' Bunyan stream — Bunyan passes the log record object directly without serialization. Bunyan numeric levels (10..60) map to breadcrumb levels the same way Pino's do.

Log4js

import 'dotenv/config';
import '@errio/node';
import log4js from 'log4js';
import { log4jsErrioAppender } from '@errio/node';

log4js.configure({
  appenders: {
    out: { type: 'stdout' },
    errio: { type: log4jsErrioAppender() },
  },
  categories: {
    default: { appenders: ['out', 'errio'], level: 'debug' },
  },
});

const log = log4js.getLogger();
log.info('user signed in', { userId: 'u_42' });
log.warn('retrying upstream', { retry: 3 });

// 1.3.0+: any Error positional arg at level >= 'error' is auto-captured.
log.error('payment failed', new Error('card declined'));

log4jsErrioAppender() returns a custom appender module that log4js v6+ accepts inline via the type field. Levels TRACE/DEBUGdebug, INFOinfo, WARNwarn, ERROR/FATALerror. Requires log4js >= 6.0.0.

Roarr

import 'dotenv/config';
import 'roarr';                       // creates globalThis.ROARR
import '@errio/node';
import { setupRoarrCapture } from '@errio/node';
import { Roarr as log } from 'roarr';

setupRoarrCapture();                  // one-shot, idempotent

log.info({ userId: 'u_42' }, 'user signed in');
log.warn({ retry: 3 }, 'retrying upstream');

// 1.3.0+: Errors in the context object at level >= 'error' are auto-captured.
// IMPORTANT: Roarr has no built-in error serializer, so you must run the
// Error through serializeRoarrError() — otherwise JSON.stringify drops the
// non-enumerable Error fields and the plugin sees an empty object.
import { serializeRoarrError } from '@errio/node';
log.error(
  { err: serializeRoarrError(new Error('payment failed')) },
  'charge declined',
);

setupRoarrCapture() patches globalThis.ROARR.write to forward each Roarr log line to a breadcrumb in addition to its original sink. It's idempotent — call it as many times as you like.

Important: call setupRoarrCapture() AFTER import 'roarr'. globalThis.ROARR only exists once Roarr's module init has run; if Roarr hasn't been imported yet, the call is a silent no-op.

Roarr only emits log lines when process.env.ROARR_LOG === 'true'. Set that in your environment to actually see (and capture) the logs.

Anthropic SDK

import 'dotenv/config';
import '@errio/node';
import Anthropic from '@anthropic-ai/sdk';
import { wrapAnthropicClient } from '@errio/node';

const client = wrapAnthropicClient(new Anthropic());

const result = await client.messages.create({
  model: 'claude-sonnet-4-5',
  max_tokens: 1024,
  messages: [{ role: 'user', content: 'hi' }],
});

wrapAnthropicClient mutates the passed client in-place and returns it. Both messages.create() and messages.stream() are instrumented to capture:

  • Prompt failures (API errors, rate limits, auth failures, mid-stream errors) — routed through the main captureError pipeline (not /api/v1/ai-events), so they land in the errors table next to everything else with grouping, dedup, issue creation, and deploy correlation. Tagged with provider=anthropic, category=ai. Includes the Anthropic request-id header in extra.anthropicRequestId for handoff to Anthropic support.
  • Latency spikes — successful calls slower than ERRIO_ANTHROPIC_LATENCY_THRESHOLD_MS (default 10000) ship to POST /api/v1/ai-events as model_timeout telemetry. Slow successes aren't really errors, so they stay on the side-channel and don't inflate your error count.
  • Model and token usage metadata.

The wrap re-throws any errors from the underlying client so calling code is unaffected. If you import '@errio/node' with @anthropic-ai/sdk installed but never call wrapAnthropicClient, the SDK warns to stderr after 5 seconds — set ERRIO_DISABLE_STARTUP_WARNINGS=true to suppress.

Breadcrumbs and async context

Breadcrumbs are stored per AsyncLocalStorage context. The Express and Fastify integrations wrap each request in such a context automatically (via requestHandler() / fastifyErrioPlugin), so log lines and console.error calls inside a request handler are correctly attached to any error captured during that request.

In a standalone script — a CLI, a cron job, a Lambda handler, a worker process — there's no per-request context wrapping your code, so addBreadcrumb and console.error (when going through the patched console plugin) silently no-op.

If you want breadcrumb capture outside of an HTTP request, opt in by wrapping your work in runWithContext:

import 'dotenv/config';
import '@errio/node';
import pino from 'pino';
import { runWithContext, pinoErrioTransport, captureError } from '@errio/node';

const log = pino({ level: 'info' }, pinoErrioTransport());

await runWithContext({ requestId: 'job-2026-04-07' }, async () => {
  log.info({ step: 'fetch' }, 'fetching upstream');
  try {
    await doWork();
  } catch (err) {
    // The pino log line above ships as a breadcrumb on this captured event.
    captureError(err);
  }
});

The context object is your free-form metadata (requestId, userId, tenantId, route, method, anything). It rides along on every event captured inside the scope.

Graceful shutdown

import { flush, shutdown } from '@errio/node';

process.on('SIGTERM', async () => {
  await flush();      // ship buffered events immediately
  await shutdown();   // tear down plugins + transport
  process.exit(0);
});

In serverless / Lambda handlers, call await flush() before returning to make sure buffered events ship before the runtime freezes.

What's in the box

  • init(config) — explicit init
  • captureError(err, options?) — manual capture with severity / tags / extra / fingerprint
  • flush(), shutdown(), getClient()
  • requestHandler(), errorHandler(), expressPlugin() — Express
  • fastifyErrioPlugin, fastifyPlugin() — Fastify
  • pinoErrioTransport(), pinoPlugin() — Pino
  • winstonErrioTransport(), winstonPlugin() — Winston
  • bunyanErrioStream(), bunyanPlugin() — Bunyan
  • log4jsErrioAppender(), log4jsPlugin() — Log4js
  • setupRoarrCapture(), roarrPlugin() — Roarr
  • wrapAnthropicClient(client), anthropicPlugin() — Anthropic SDK
  • consolePlugin() — auto-registered, patches console.error / console.warn
  • runWithContext, setContext, getContext, addBreadcrumb, getBreadcrumbs, clearBreadcrumbs — context primitives

License

MIT