Tom Ryan / A5omic

Silent defects

A defect that throws gets reported, and then it gets fixed. A defect that returns a plausible wrong answer does not get reported, because nobody notices. These are the second kind, in packages that between them are downloaded more than 34.4M times a week.

Every finding below was reproduced locally against the version named beside it, which was the latest release at the time. None of it is inferred from documentation or changelogs. npm audit reports none of them, because none of them is a vulnerability.

Each project's own tracker was searched before publishing. Nothing on this page has a prior report, is fixable by a documented setting, or is a matter of convention — findings that failed any of those tests were removed. What remains is short on purpose: four defects where "this is wrong, and nobody has said so" is simply true.

npx silent-defects checks your own dependencies against this list.

4 findings, 2 of them high severity. Measured on Node v24.21.0, 2026-09-16/19.

high serialize-error 24.4M/week

deserializeError is exponential in the depth of the cause chain

0.4 ms at depth 5, 10.6 ms at 10, 339 ms at 15 and 11 seconds at depth 20, on a payload of 6.6 KB. In destroyCircular, `cause` is walked once in the Object.keys loop and again in the errorProperties loop, and seen.delete(from) makes cycle detection path-based rather than memoised, so the two paths compound to 2^depth. Serializing is fast because `cause` is non-enumerable on a real Error; a JSON round trip makes it enumerable, which is exactly the worker-to-main-thread path the library exists for. A separate performance problem in the same function — copying the `seen` array on every recursion — was reported and fixed in 13.0.1 (#116); this one is present in that release and was not part of that report.

const {serializeError, deserializeError} = require('serialize-error');
let e = new Error('root');
for (let i = 1; i < 20; i++) e = new Error('layer ' + i, {cause: e});
console.time('t');
deserializeError(JSON.parse(JSON.stringify(serializeError(e))));
console.timeEnd('t');  // ~11 seconds

Applies to you if you wrap errors with `cause` through several layers and send them across a boundary as JSON.

Measured against serialize-error@13.0.1. Cap the cause chain before serialising, or use a serialiser that handles errors iteratively.

No prior report of this was found in the project's tracker.

high parse-duration 575K/week

parse-duration drops the day from a valid ISO 8601 duration, and reads P1M as one minute

P1DT2H returns two hours; the day is gone. And P1M (one month) and PT1M (one minute) both return 60000, because M means months before the T and minutes after it. A monthly retention window becomes a one-minute one. No setting changes this. ISO 8601 support was added in response to feature requests (#30, #33); the wrong output has not been reported.

const p = require('parse-duration');
p('P1DT2H')  // 7200000 — should be 93600000
p('P1M') === p('PT1M')  // true

Applies to you if you accept ISO 8601 durations from an API, a JSON Schema, or a calendar feed.

Measured against parse-duration@2.1.8. Parse ISO durations with a dedicated ISO parser.

No prior report of this was found in the project's tracker.

medium croner 6.8M/week

croner fires an hourly job twice at the same instant across spring forward

'0 * * * *' in America/New_York on 2026-03-08 yields 24 fires for a 23-hour day, emitting 2026-03-08T07:00:00Z twice. cron-parser gives 23 for the same expression. croner's maintainer has fixed several daylight-saving problems over the years (#142, #284, #286, #343); this one is a different symptom from all of them and reproduces on the latest stable release and on the 10.0.2-dev.3 and 11.0.0-dev.0 prereleases that carry those fixes.

const {Cron} = require('croner');
const runs = new Cron('0 * * * *', {timezone: 'America/New_York'})
  .nextRuns(80, new Date('2026-03-07T12:00:00Z'));
// two entries share the instant 2026-03-08T07:00:00.000Z

Applies to you if you run sub-daily jobs in a timezone that observes daylight saving, and running twice is not safe.

Measured against croner@10.0.1. Make the job idempotent, or use cron-parser to compute occurrences.

No prior report of this was found in the project's tracker.

medium rrule 2.7M/week

rrule returns the wrong instant when the machine's own timezone changes on the same day as the target zone

A daily 09:00 recurrence in America/New_York is correct every day except the spring-forward day, where it points an hour early — 08:00 instead of 09:00. rrule's documented model routes a TZID result through the system timezone, and the error appears only when the system zone also transitions that day. Measured correct under TZ=UTC, Asia/Tokyo and Europe/London, and wrong under America/Los_Angeles. On a UTC container it is right, so it passes in CI and fires early in production once a year.

// with the system timezone set to America/Los_Angeles
const {rrulestr} = require('rrule');
const dates = rrulestr('DTSTART;TZID=America/New_York:20260305T090000\nRRULE:FREQ=DAILY;COUNT=6').all();
// 2026-03-08 comes back an hour early: 08:00 New York, not 09:00

Applies to you if you schedule recurrences with a TZID and your process runs in a timezone that observes daylight saving.

Measured against rrule@2.8.1. Run the process in UTC, or use rrule-temporal, which was measured correct on all of these.

No prior report of this was found in the project's tracker.

How this list is maintained

Each finding names the exact version its behaviour was observed on. If a library fixes the behaviour, the finding becomes wrong and should be removed — a report is welcome, and the check suite that backs this page runs every claim before it is published.

A listed defect does not mean you have a bug. Depending on croner is not a problem; running an hourly job in a daylight-saving zone where a second run is unsafe is. Each entry says which condition matters so you can decide for yourself.