MOMENT.JS: A POST-MORTEM
README.md
Moment.js is a legacy project, now in maintenance mode. In most cases, you should choose a different library.
That's right. The MAINTAINERS are telling you to use something else.
This isn't a roast. This is an autopsy. The body is still warm, still getting npm downloads, still haunting production codebases worldwide. And we're about to find out why.
5,688 lines. 375 functions. 176KB unminified. 137 locales including KLINGON. And somewhere in that chaos, a comment that simply reads:
// TODO: Another silent failure?
Welcome to Moment.js. Where dates go to mutate.
BIT #1: THE SACRED TESTS OF MUTATION
src/test/moment/mutable.js:
test('manipulation methods', function (assert) {
var m = moment();
assert.equal(m, m.year(2011), 'year() should be mutable');
assert.equal(m, m.month(1), 'month() should be mutable');
assert.equal(m, m.hours(7), 'hours() should be mutable');
assert.equal(m, m.minutes(33), 'minutes() should be mutable');
assert.equal(m, m.seconds(44), 'seconds() should be mutable');
assert.equal(m, m.milliseconds(55), 'milliseconds() should be mutable');
assert.equal(m, m.day(2), 'day() should be mutable');
assert.equal(m, m.startOf('week'), 'startOf() should be mutable');
assert.equal(m, m.add(1, 'days'), 'add() should be mutable');
assert.equal(m, m.subtract(2, 'years'), 'subtract() should be mutable');
Read those assertion messages.
"year() SHOULD BE mutable"
"month() SHOULD BE mutable"
"add() SHOULD BE mutable"
They're not testing IF it's mutable. They're ASSERTING that mutability is CORRECT. This isn't a test suite. This is a manifesto. This is religious scripture.
Somewhere in 2013, someone decided that when you ask for the start of the week, the date you're holding should TRANSFORM. Not return a new date. BECOME a different date. And then they wrote a test to make sure nobody ever fixes it.
The only non-mutable method?
assert.notEqual(m, m.clone(), 'clone() should not be mutable');
clone(). The one method that does what every method should do.
Every single bug report about "my date changed unexpectedly" traces back to this file. Every single one. And it's PROTECTED BY TESTS.
BIT #2: THE SILENT FAILURES
moment.js:1260:
value = mom.localeData().monthsParse(value);
// TODO: Another silent failure?
if (!isNumber(value)) {
return mom;
}
"Another silent failure?"
ANOTHER?!
The developer who wrote this KNEW they were adding a silent failure. They knew there were OTHERS. They left a TODO about it. And then shipped it anyway.
What does this code do? If you pass an invalid month string, it just... returns the moment unchanged. No error. No warning. No indication anything went wrong. Your code keeps running with the wrong date.
That "?" at the end kills me. It's not even confident about being a silent failure. It's ASKING if this is a silent failure. Like the developer was having a philosophical debate with themselves while writing production code.
The fallback system is even better:
moment.js:2617:
hooks.createFromInputFallback = deprecate(
'value provided is not in a recognized RFC2822 or ISO format.
moment construction falls back to js Date(), which is not
reliable across all browsers and versions.',
function (config) {
config._d = new Date(config._i + (config._useUTC ? ' UTC' : ''));
}
);
Translation: "If we can't parse your date, we'll just throw it at the native Date constructor and pray. Yes, the same Date constructor that moment.js was created to FIX."
Full circle.
BIT #3: "THE MIN/MAX IDENTITY CRISIS"
moment.js:3107-3128:
var prototypeMin = deprecate(
'moment().min is deprecated, use moment.max instead.',
function () {
var other = createLocal.apply(null, arguments);
if (this.isValid() && other.isValid()) {
return other < this ? this : other;
} else {
return createInvalid();
}
}
),
prototypeMax = deprecate(
'moment().max is deprecated, use moment.min instead.',
Read that again.
"moment().min is deprecated, use moment.max instead."
"moment().max is deprecated, use moment.min instead."
To fix min(), use max().
To fix max(), use min().
This is Kafka. This is Catch-22. This is a library that looked at its own API and said "we named these backwards, and instead of fixing it, we'll deprecate both and tell users to swap them."
Somewhere there's a developer who saw the deprecation warning for min(), changed it to max() like the message said, got the OPPOSITE behavior, saw another deprecation warning telling them to use min(), and is now questioning their entire understanding of mathematics.
BIT #4: THE TODOS THAT BECAME FEATURES
moment.js:369 and 4775:
// TODO: Remove "ordinalParse" fallback in next major release.
moment.js:2168:
// mark as not found to avoid repeating expensive file require call
moment.js (multiple locations):
// TODO: Find a better way to register and load all the locales in Node
// TODO: Replace the vanilla JS Date object with an independent check.
// TODO: We need to take the current isoWeekYear, but that depends on
// TODO: Move this to another part of the creation flow
// TODO: Use [].sort instead?
// TODO: remove 'name' arg after deprecation is removed
"Remove ordinalParse fallback in next major release."
The library is at version 2.30.1. They've had 30 minor releases to remove this. THIRTY. And it's still there. Twice.
"next major release" - a phrase that means "never" in moment.js.
The last major release was 2.0.0 on February 9, 2013. That's right. Version 2.0.0 came out when Obama was president. When people still used Internet Explorer 8. When "responsive design" was cutting edge.
Every TODO in this codebase is a promise made to a future that will never come. They're not todos. They're epitaphs.
src/test/moment/locale.js:1002:
// TODO: Enable this after fixing pl months parse hack hack
"hack hack" - not a typo. There's a hack for Polish months. And then a HACK ON TOP OF THE HACK. And this test has been commented out, waiting for someone to un-hack the hack-hack.
That day is never coming.
BIT #5: QAPLA'! THE KLINGON LOCALE
locale/tlh.js:
//! locale : Klingon [tlh]
//! author : Dominika Kruk : https://github.com/amaranthrose
var numbersNouns = 'pagh_wa'_cha'_wej_loS_vagh_jav_Soch_chorgh_Hut'.split('_');
months: 'tera' jar wa'_tera' jar cha'_tera' jar wej_tera' jar loS_
tera' jar vagh_tera' jar jav_tera' jar Soch_tera' jar chorgh_
tera' jar Hut_tera' jar wa'maH_tera' jar wa'maH wa'_
tera' jar wa'maH cha''
weekdays: 'lojmItjaj_DaSjaj_povjaj_ghItlhjaj_loghjaj_buqjaj_ghInjaj'
relativeTime: {
s: 'puS lup', // "a few seconds" in Klingon
m: 'wa' tup', // "one minute"
h: 'wa' rep', // "one hour"
d: 'wa' jaj', // "one day"
y: 'wa' DIS', // "one year"
}
Someone looked at moment.js and thought: "You know what this library for parsing human dates needs? Support for a fictional language spoken by aliens in a 1960s TV show."
And someone REVIEWED THIS PR. Someone clicked "Approve". Multiple people had to agree that yes, enterprise applications definitely need to display "tera' jar chorgh" instead of "August."
There are 137 locales in moment.js. Klingon is one of them.
You know what's NOT a locale? Latin. Ancient Greek. Sumerian. Actual languages that humans used to conduct actual historical business.
But Klingon made the cut.
Because when your library is already 176KB, what's another locale for a language with maybe 30 fluent speakers on Earth?
Today is ghInjaj. The time is wa'maH cha' rep. Your sprint is doomed.
BIT #6: THE PSEUDO LOCALE
locale/x-pseudo.js:
//! locale : Pseudo [x-pseudo]
//! author : Andrew Hood
months: 'J~ánúá~rý_F~ébrú~árý_~Márc~h_Áp~ríl_~Máý_~Júñé~_Júl~ý_
Áú~gúst~_Sép~témb~ér_Ó~ctób~ér_Ñ~óvém~bér_~Décé~mbér'
relativeTime: {
future: 'í~ñ %s',
past: '%s á~gó',
s: 'á ~féw ~sécó~ñds',
m: 'á ~míñ~úté',
h: 'á~ñ hó~úr',
d: 'á ~dáý',
}
calendar: {
sameDay: '[T~ódá~ý át] LT',
nextDay: '[T~ómó~rró~w át] LT',
lastDay: '[Ý~ést~érdá~ý át] LT',
}
It's English. But drunk. And wearing a fake mustache.
This is a "pseudo-localization" locale for testing i18n. The idea is you run your app with this locale to spot untranslated strings.
A reasonable developer would generate this programmatically.
The moment.js team? They hand-typed 85 lines of drunk English and committed it to the repo. Forever. Shipping to every user who doesn't tree-shake their imports.
"T~ómó~rró~w" - a word that has never been typed by a human before or since, except in this file, which will now be downloaded by npm approximately 15 million times per week until the heat death of the universe.
BIT #7: "THE FLOATING POINT CONFESSION"
this._milliseconds =
+milliseconds +
seconds * 1e3 + // 1000
minutes * 6e4 + // 1000 * 60
hours * 1000 * 60 * 60; //using 1000 * 60 * 60 instead of 36e5
//to avoid floating point rounding errors
//https://github.com/moment/moment/issues/2978
Let me translate this comment for you:
"We can't use 36e5 (scientific notation for 3,600,000) because JavaScript floating point math is broken, so instead we multiply 1000 * 60 * 60 which... is also JavaScript floating point math."
But wait, it gets better:
output = (this - that) / 36e5;
THREE LINES LATER, they use 36e5 anyway.
The same file. The same library. One place carefully avoids 36e5 with a GitHub issue link explaining why. Another place uses it directly without a care in the world.
Someone fixed the bug. Someone else didn't get the memo. The code shipped. The bug lives in some code paths but not others. And the comment remains, a monument to inconsistency.
BIT #8: IE8 FOREVER
// IE8 will treat undefined and null as object if it wasn't for
// input != null
// IE8 Quirks Mode & IE7 Standards Mode do not allow accessing
// strings like arrays
// Using charAt should be more compatible.
Internet Explorer 8 was released in 2009. Microsoft ended support for it in 2016.
It is now 2025. We're on version 2.30.1. These comments are still here. This code is still running. Every single person who imports moment.js is paying the cost of IE8 compatibility.
"Using charAt should be more compatible."
Compatible with WHAT? IE7? A browser from 2006? A browser that was deprecated when OBAMA was in his FIRST TERM?
Somewhere right now, a React developer with a 2025 MacBook Pro is bundling code that handles IE7 quirks mode. Their lighthouse score suffers. Their users wait an extra 50 milliseconds. And moment.js smiles from its 176KB throne.
The README says "legacy project, now in maintenance mode."
Maintenance mode means "we will never remove the IE8 hacks, but we will also never need to." It's perfect. It's eternal. It's shipping to production forever.
BIT #9: THE VOID
moment.js:1271:
void (mom._isUTC
? mom._d.setUTCMonth(month, date)
: mom._d.setMonth(month, date));
moment.js:976:
return void (isUTC
? d.setUTCMilliseconds(value)
: d.setMilliseconds(value));
moment.js:980:
return void (isUTC ? d.setUTCSeconds(value) : d.setSeconds(value));
moment.js:982:
return void (isUTC ? d.setUTCMinutes(value) : d.setMinutes(value));
moment.js:984:
return void (isUTC ? d.setUTCHours(value) : d.setHours(value));
moment.js:986:
return void (isUTC ? d.setUTCDate(value) : d.setDate(value));
They use void to explicitly discard return values.
Why? Because the native Date setters return timestamps, and moment.js doesn't want to return those. Fair enough.
But they wrote it as a ternary expression INSIDE void, turning "set a value" into a golf challenge. There's no reason for this. A simple if/else would be clearer. Two statements would be readable.
But no. We must return void of a ternary that mutates state as a side effect. Because if you're going to write confusing code, you might as well commit to it.
228 uses of this._ in this file.
114 uses of config._.
Internal state sprayed everywhere like a Jackson Pollock painting.
The underscore prefix means "private" in JavaScript convention. But there's no actual privacy. It's just vibes. It's a gentleman's agreement that nobody respects.
BIT #10: THE DEPRECATION GRAVEYARD
moment.js:4991-5009:
proto.dates = deprecate(
'dates accessor is deprecated. Use date instead.',
getSetDayOfMonth
);
proto.months = deprecate(
'months accessor is deprecated. Use month instead',
getSetMonth
);
proto.years = deprecate(
'years accessor is deprecated. Use year instead',
getSetYear
);
proto.zone = deprecate(
'moment().zone is deprecated, use moment().utcOffset instead.',
getSetZone
);
proto.isDSTShifted = deprecate(
'isDSTShifted is deprecated.',
isDaylightSavingTimeShifted
);
proto.lang = deprecate(
'moment().lang() is deprecated. Use moment().localeData().',
function () { ... }
);
Count them. Six deprecated methods on the prototype. Still there. Still callable. Still shipping.
"Use date instead of dates" - for when you wanted to type 5 characters but accidentally typed 6. Thank god they deprecated this. It was chaos.
The deprecation function itself (line 291) just logs a console warning. Once. Then sets a flag to never warn again. So if you miss that ONE warning in your console, you'll never know you're using deprecated APIs.
They deprecated methods. They didn't remove them. They will NEVER remove them. This is "maintenance mode" - the code is frozen in amber, warnings and all, forever.
Every deprecation warning is a tiny ghost, whispering "I should be deleted" into the void. Nobody is listening.
BIT #11: THE CREATE.JS TEST FILE
src/test/moment/create.js: 2,919 lines
Two thousand nine hundred and nineteen lines. For ONE test file. Testing ONE function: creating a moment.
For comparison:
- The entire Vue.js reactivity system is ~1,500 lines
- The React reconciler is ~2,000 lines
- Most entire applications are smaller than this test file
test('array', function (assert) {
assert.ok(moment([2010]).toDate() instanceof Date, '[2010]');
assert.ok(moment([2010, 1]).toDate() instanceof Date, '[2010, 1]');
assert.ok(moment([2010, 1, 12]).toDate() instanceof Date, '[2010, 1, 12]');
...
});
Every possible way to create a moment. Every edge case. Every format. Every locale interaction. Every invalid input. All tested. All in one file. Scrolling through it is like reading the Old Testament.
And here's the thing: all these tests are PROTECTING the broken behavior. Every mutation. Every silent failure. Every weird edge case. All enshrined in 2,919 lines of test code that say "yes, this is exactly how it should work."
This isn't a test suite. It's a legal document. It's proof that every bug is intentional.
THE FINAL WORD
moment.js is not a bad library. It's a HISTORICALLY IMPORTANT bad library.
It solved a real problem in 2011 when JavaScript dates were unusable. It became the standard. It got installed billions of times. It taught an entire generation of developers that dates are hard.
And then it taught them that dates should mutate.
And that silent failures are acceptable.
And that 176KB is a reasonable size for a date library.
And that deprecation means "please ignore this warning."
And that IE8 compatibility matters in 2025.
And that Klingon is a production-ready locale.
The maintainers know. They put it right in the README:
"In most cases, you should choose a different library."
This is a library that comes with a warning label FROM ITS OWN AUTHORS telling you not to use it.
But here's the real roast: you're still using it.
Somewhere in your node_modules, moment.js is lurking. Maybe a dependency of a dependency. Maybe some legacy code nobody wants to touch. Maybe you just never got around to migrating.
And every time you run npm install, you download:
- 5,688 lines of mutable date chaos
- 137 locales including Klingon and drunk English
- 13+ years of TODOs that will never be fixed
- IE8 compatibility you will never need
- Tests that assert bad behavior is correct
176KB of history. 176KB of lessons learned. 176KB of "we should really migrate to date-fns."
But you won't. Because the tests pass. Because it works. Because migration is hard and sprint is short and prod is stable.
And moment.js knows it. That's why it's still here.
That's why it will always be here.
Until the heat death of the universe.
Or until someone finally writes that migration script.
Whichever comes first.
DaHjaj QaQ
"Today is good"
- moment('tlh')