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

obviate

v0.1.2

Published

Minimal promise circuit breaker — trip after N failures, fail fast during cooldown, probe once before reopening. Zero deps.

Readme

obviate

Minimal promise circuit breaker — trip after N failures, fail fast during cooldown, probe once before reopening. Zero deps.

Overview

A circuit breaker protects distributed systems from cascading failures by failing fast when a dependency is experiencing issues. When wrapped in a circuit breaker, function calls are automatically blocked after consecutive failures, allowing the downstream service time to recover.

Installation

npm install obviate
# or
pnpm add obviate
# or
yarn add obviate

Quick Start

import obviate from "obviate";

// Wrap an async function
const fetchUser = obviate(async (id: string) => {
  const response = await fetch(`/api/users/${id}`);
  return response.json();
}, {
  threshold: 5,      // trip after 5 consecutive failures
  cooldownMs: 30000  // stay open for 30 seconds
});

// Use normally
try {
  const user = await fetchUser("42");
  console.log(user);
} catch (error) {
  if (error.message === "Circuit breaker is open") {
    console.log("Service temporarily unavailable");
  }
}

How It Works

The circuit breaker has three states:

  1. Closed (default): Calls pass through normally. Consecutive failures increment a counter.

  2. Open: After threshold failures, the breaker trips. All calls fail immediately with BreakerOpen error without invoking the wrapped function.

  3. Half-open: After cooldown period, the next call is allowed through as a "probe". If successful, the breaker closes and resets. If it fails, the breaker opens again.

API

obviate(fn, options)

Creates a circuit breaker that wraps an async function.

Parameters:

  • fn: The async function to wrap
  • options: Configuration options (optional)

Returns: A circuit breaker function with state and methods

Options

interface BreakerOptions {
  /** Consecutive failures to trip the breaker. Default: 5 */
  threshold?: number;

  /** Duration in milliseconds before attempting half-open probe. Default: 30000 */
  cooldownMs?: number;

  /** Determine if an error should count as a failure. Default: any throw counts */
  isFailure?: (error: unknown) => boolean;

  /** Callback fired when breaker state changes */
  onStateChange?: (from: BreakerState, to: BreakerState) => void;

  /** Test seam for time injection. Default: Date.now */
  clock?: () => number;
}

Circuit Breaker Interface

The returned circuit breaker has the following properties and methods:

interface Breaker<A extends unknown[], R> {
  /** Execute the wrapped function with circuit breaker logic */
  (...args: A): Promise<R>;

  /** Current state: "closed" | "open" | "half-open" */
  readonly state: BreakerState;

  /** Number of consecutive failures counted */
  readonly failures: number;

  /** Manually trip the breaker to open state */
  trip(): void;

  /** Manually reset the breaker to closed state */
  reset(): void;
}

BreakerOpen Error

Thrown when the circuit breaker is in open state:

import { BreakerOpen } from "obviate";

try {
  await breaker();
} catch (error) {
  if (error instanceof BreakerOpen) {
    console.log("Circuit breaker is open");
  }
}

Examples

Basic Usage

import obviate from "obviate";

const apiCall = obviate(async (endpoint: string) => {
  const response = await fetch(endpoint);
  if (!response.ok) throw new Error(`HTTP ${response.status}`);
  return response.json();
}, {
  threshold: 3,
  cooldownMs: 10000
});

// Normal operation
const data = await apiCall("/api/data");

// After 3 failures, circuit opens
// All subsequent calls fail immediately with BreakerOpen

Selective Failure Counting

Ignore certain errors (like 404) from tripping the breaker:

const smartBreaker = obviate(async (url: string) => {
  const response = await fetch(url);
  if (response.status === 404) {
    const error = new Error("Not found");
    (error as any).statusCode = 404;
    throw error;
  }
  return response.json();
}, {
  threshold: 5,
  isFailure: (error) => {
    // Don't count 404 errors toward tripping
    const err = error as { statusCode?: number };
    return err.statusCode !== 404;
  }
});

Manual Control

Force the breaker open based on external health checks:

const breaker = obviate(fetchUserData, { threshold: 3 });

// External health check fails
breaker.trip();

// Later, when service recovers
breaker.reset();

State Monitoring

Track state changes for monitoring and alerting:

const breaker = obviate(apiCall, {
  threshold: 3,
  cooldownMs: 30000,
  onStateChange: (from, to) => {
    console.log(`Circuit breaker: ${from} → ${to}`);
    if (to === "open") {
      // Send alert to monitoring system
      alertService.notify("Circuit breaker opened");
    }
  }
});

Deterministic Testing

Inject a custom clock for predictable testing:

let testClock = 0;

const breaker = obviate(failingFunction, {
  threshold: 2,
  cooldownMs: 1000,
  clock: () => testClock
});

// Control time for testing
testClock = 0;
await breaker();  // failure 1
await breaker();  // failure 2, trips to open

testClock = 1001; // advance past cooldown
await breaker();  // half-open probe

Use Cases

Protecting External APIs

const paymentApi = obviate(async (paymentData) => {
  return await fetch("/api/payments", {
    method: "POST",
    body: JSON.stringify(paymentData)
  });
}, {
  threshold: 3,
  cooldownMs: 60000,  // 1 minute cooldown
  onStateChange: (from, to) => {
    if (to === "open") {
      logger.error("Payment API circuit opened");
    }
  }
});

Database Connection Protection

const dbQuery = obviate(async (sql) => {
  return await database.query(sql);
}, {
  threshold: 5,
  cooldownMs: 30000,
  isFailure: (error) => {
    // Only count connection errors, not query errors
    return error.message.includes("connection");
  }
});

Microservice Resilience

// Different thresholds for different service tiers
const criticalService = obviate(apiCall, {
  threshold: 2,   // low tolerance
  cooldownMs: 10000
});

const backgroundService = obviate(apiCall, {
  threshold: 10,  // high tolerance
  cooldownMs: 60000
});

Best Practices

Set Appropriate Thresholds

  • Critical services: Lower threshold (2-3 failures)
  • Non-critical services: Higher threshold (5-10 failures)
  • Consider your service's typical failure patterns

Configure Cooldown Based on Recovery Time

  • Set cooldown longer than typical service recovery time
  • Too short: May overwhelm recovering service
  • Too long: Unnecessary delays in attempting recovery

Use isFailure Selectively

  • Don't count client errors (400, 404) toward tripping
  • Focus on server errors (500, 503) that indicate service issues
  • Consider network errors vs. application errors

Monitor State Changes

  • Log all state transitions for observability
  • Alert on circuit opens for critical services
  • Track how often circuits trip to identify systemic issues

Test Failure Scenarios

  • Use clock injection for deterministic testing
  • Test half-open recovery behavior
  • Verify concurrent call handling

Limitations

This is a minimal circuit breaker implementation focused on core functionality. It does not include:

  • Request queuing: Calls during open state fail immediately; no queueing
  • Metrics collection: No built-in metrics (use external monitoring)
  • Automatic retry: No retry logic (use with a retry library)
  • Timeout handling: Does not add timeouts to wrapped functions
  • Rolling windows: Uses consecutive failure count, not time-based windows
  • Distributed coordination: Each breaker instance is independent

Error Handling

The circuit breaker preserves error semantics:

  • Closed state: Original errors are re-thrown
  • Open state: BreakerOpen error is thrown immediately
  • Half-open state: Original errors are re-thrown, breaker re-opens
try {
  await breaker();
} catch (error) {
  if (error instanceof BreakerOpen) {
    // Circuit is open, call blocked
    handleCircuitOpen();
  } else {
    // Original error from wrapped function
    handleOriginalError(error);
  }
}

Testing

The library supports deterministic testing through clock injection:

describe("my circuit breaker", () => {
  it("should trip after threshold failures", async () => {
    const clock = { now: 0 };

    const breaker = obviate(async () => {
      throw new Error("fail");
    }, {
      threshold: 3,
      clock: () => clock.now
    });

    await breaker().catch(() => {}); // failure 1
    await breaker().catch(() => {}); // failure 2
    await breaker().catch(() => {}); // failure 3

    expect(breaker.state).toBe("open");
  });
});

License

MIT

Related Packages

Caching & Concurrency:

Text Processing:

  • @azghr/shorn — Truncate strings by byte budget without breaking graphemes
  • seriatim — Sequential processing utilities

HTTP & Network:

  • forbear — Read server rate-limit instructions from HTTP responses
  • forestall — Delay execution until a condition is met

System & Process:

  • quiesce — Ordered, timeboxed graceful shutdown for Node
  • sortition — Deterministic percentage rollouts and A/B bucketing
  • stanch — Stop flows or operations based on conditions

Utilities:

  • expunge — Remove or exclude items from collections
  • occlude — Hide or mask data and functionality
  • placemark — Geographic location and mapping utilities
  • specie — Currency and financial calculations

Contributing

Contributions are welcome! Please ensure all tests pass and maintain the minimalist philosophy of this package.

Keywords

circuit-breaker, resilience, retry, fault-tolerance, fail-fast, promise, async, breaker, recovery, threshold