@js-joda/core review
@js-joda/core 6.1.0 models dates and time with immutable Java-style value classes instead of putting every concept into `Date`. `LocalDate` stores a calendar day, `LocalDateTime` stores wall time, `Instant` marks a timeline point, `Period` does calendar arithmetic, and `Duration` measures elapsed time. This release fixes offset parsing in `Instant.parse`, negative duration strings, pre-epoch instant truncation, overlapping zone-ID prefixes, and declaration errors. Named IANA zones and localized text still require separate js-joda packages. Our full import weighed 193 KB minified and 40.3 KB gzipped.
Our @js-joda/core 6.1.0 install had zero dependencies and zero audit findings, but the full browser import measured 40.3 KB gzipped. Install it when separate date, local-time, instant, period, and duration types prevent real domain mistakes; use native Date helpers or Luxon for a smaller conceptual job.
We installed it
| Install | ✓ · 1.4s | 1 package on disk · 8 MB |
| Import | ✓ | ESM import works · require() works · CommonJS package |
| Browser | 40.3 KB | gzipped (193 KB minified), bundled with esbuild |
| Types | ✓ | TypeScript types bundled |
| Known vulns | 0 | 0 critical · 0 high · 0 moderate · 0 low (npm audit) |
Answers from our run
Does @js-joda/core install cleanly?
Yes. In a fresh container with an empty cache, npm install @js-joda/core finished in 1 seconds, leaving 1 package and 8 MB on disk. npm audit reported no known vulnerabilities.
How much does @js-joda/core add to a browser bundle?
40.3 KB gzipped (193 KB minified) when the whole package is bundled for the browser with esbuild. Importing only part of it is usually smaller.
Does @js-joda/core work with both ESM and CommonJS?
Yes. Both import '@js-joda/core' and require('@js-joda/core') worked in Node 22 in our run. The package is published as CommonJS.
Does @js-joda/core include TypeScript types?
Yes, type declarations ship inside the package, so no @types install is needed.
@js-joda/core or luxon: which should you use?
luxon: Choose it for immutable date-times with zones and locales exposed through an API shaped more like ordinary JavaScript. Our @js-joda/core 6.1.0 install had zero dependencies and zero audit findings, but the full browser import measured 40.3 KB gzipped.
When should you not use @js-joda/core?
You only need a few operations on native Date. A 40.3 KB gzipped full import and a broad class model are hard to justify for simple display formatting.
Use it if
- Your database must keep a birthday, billing day, local appointment, and timestamp as different domain values.
- Developers already know Java's `java.time` API and want the same mental model in JavaScript or TypeScript.
- Calendar calculations need immutable `Period`, adjusters, explicit clocks, or nanosecond-capable `Instant` values.
- ISO parsing should reject impossible dates instead of relying on `Date` normalization and machine-local defaults.
- You only need a few operations on native `Date`. A 40.3 KB gzipped full import and a broad class model are hard to justify for simple display formatting.
- Named zones must work from one package. Core needs `@js-joda/timezone` to register IANA rules such as `Europe/Berlin`.
- Localized month or weekday names are central. Those require `@js-joda/locale` plus registered locale data, outside this package.
- Internet Explorer 11 remains in scope. The 6.0 changelog explicitly removed IE11 support.
- Users enter loose phrases such as `next Friday evening`. The documented parsers consume ISO text or declared formatter patterns, not natural language.
- Your domain uses non-ISO calendars. The project states that core follows the ISO calendar under proleptic Gregorian rules.
Setup reality
Our fresh install of @js-joda/core 6.1.0 took 1.4 seconds and put one package on disk using 8 MB. Its unpacked contents total 7800 KB, with zero direct dependencies and zero peers. npm audit returned zero known vulnerabilities. The CommonJS package has no exports map; require() and ESM import worked in Node 22. TypeScript declarations are bundled.
The first setup decision is the value type, not a config file. LocalDateTime has no offset and cannot identify a unique instant during a daylight-saving overlap. Instant is absolute but has no human calendar context. Attach an explicit ZoneId or ZoneOffset at the boundary where local input becomes a timestamp. Avoid letting the server's system zone decide business data unless that is the written rule.
Core carries fixed offsets and a system zone, while IANA rules come from @js-joda/timezone. Import that package for its registration side effect before resolving named zones. Locale formatting follows a similar split: import Locale from @js-joda/locale, then import a locale data package to register names. Version 6 changed that locale import arrangement and removed IE11. No credentials or native compilation are involved, but deployed bundles must include every registration import.
All arithmetic returns another value. plusMonths can adjust an invalid target day to the last valid day, while Duration counts time and Period counts calendar units. Parsing and invalid-field errors throw js-joda exceptions. Our browser check reached 193 KB minified and 40.3 KB gzipped for import *, so browser code should import only the classes it uses and confirm the actual production chunk. Native Date conversions also discard Instant precision below 1 millisecond.
Patterns
Keep a date without time or zone parse-calendar-date
import { LocalDate } from '@js-joda/core'
const dueDate = LocalDate.parse('2026-09-30')
console.log(dueDate.year(), dueDate.monthValue(), dueDate.dayOfMonth())
console.log(dueDate.toString())`LocalDate` stores 2026-09-30 without inventing midnight or a machine time zone.
Construct a checked calendar value reject-invalid-date
import { LocalDate } from '@js-joda/core'
const releaseDate = LocalDate.of(2026, 8, 25)`LocalDate.of` throws `DateTimeException` for an impossible combination such as month 2 and day 30.
Move a billing date by calendar months add-calendar-months
const opened = LocalDate.parse('2026-01-31')
const nextCycle = opened.plusMonths(1)
console.log(opened.toString())
console.log(nextCycle.toString())The original value stays 2026-01-31; `plusMonths(1)` resolves to the last valid day of February.
Compare two LocalDate values compare-dates
const today = LocalDate.parse('2026-08-25')
const cutoff = LocalDate.parse('2026-09-01')
if (today.isBefore(cutoff)) console.log('accepting entries')Use `isBefore`, `isAfter`, `isEqual`, or `compareTo` instead of JavaScript object comparison.
Choose calendar units or total days measure-calendar-gap
import { ChronoUnit, LocalDate, Period } from '@js-joda/core'
const start = LocalDate.parse('2026-01-31')
const end = LocalDate.parse('2026-03-02')
const calendarGap = Period.between(start, end)
const totalDays = start.until(end, ChronoUnit.DAYS)`Period` preserves year, month, and day components; `ChronoUnit.DAYS` returns one total day count.
Represent a local appointment parse-wall-time
import { LocalDateTime } from '@js-joda/core'
const appointment = LocalDateTime.parse('2026-10-25T09:30:00')
const reminder = appointment.minusMinutes(20)A `LocalDateTime` has no offset, so this 09:30 value is not an absolute timestamp until a zone is attached.
Resolve an offset timestamp to an Instant parse-offset-instant
import { Instant } from '@js-joda/core'
const event = Instant.parse('2026-08-25T14:00:00+02:00')
console.log(event.toString())Core 6.1.0 fixed offset acceptance in `Instant.parse`; the value is resolved onto the UTC timeline.
Register and use IANA zone rules use-named-zone
import '@js-joda/timezone'
import { LocalDateTime, ZoneId } from '@js-joda/core'
const meeting = LocalDateTime.parse('2026-11-02T09:00')
.atZone(ZoneId.of('Europe/Berlin'))Install and import `@js-joda/timezone` before calling `ZoneId.of` with an IANA name; core alone does not carry that database.
Use an explicit numeric pattern format-numeric-date
import { DateTimeFormatter, LocalDate } from '@js-joda/core'
const format = DateTimeFormatter.ofPattern('dd/MM/uuuu')
const date = LocalDate.parse('25/08/2026', format)
console.log(date.format(format))Core can format numeric ISO fields. Localized names need `@js-joda/locale` and registered locale data.
Apply a calendar adjuster find-month-end
import { LocalDate, TemporalAdjusters } from '@js-joda/core'
const anyDay = LocalDate.parse('2028-02-10')
const monthEnd = anyDay.with(TemporalAdjusters.lastDayOfMonth())
console.log(monthEnd.toString())The leap-year result is 2028-02-29, and `anyDay` remains unchanged because the values are immutable.
Make now deterministic in a test freeze-clock
import { Clock, Instant, LocalDate, ZoneOffset } from '@js-joda/core'
const clock = Clock.fixed(
Instant.parse('2026-08-25T23:45:00Z'),
ZoneOffset.UTC
)
const today = LocalDate.now(clock)Passing a fixed `Clock` controls both the instant and zone without patching global timers.
Cross the native Date boundary convert-native-date
import { convert, nativeJs } from '@js-joda/core'
const zoned = nativeJs(new Date('2026-08-25T12:00:00Z'))
const dateAgain = convert(zoned).toDate()Native Date stores milliseconds, so a round trip cannot preserve `Instant` precision finer than 1 millisecond.
Alternatives
| Package | Registry | Pick it when |
|---|---|---|
| luxon | npm | Choose it for immutable date-times with zones and locales exposed through an API shaped more like ordinary JavaScript. |
| date-fns | npm | Choose individual functions around native Date when bundle selection and straightforward helpers matter most. |
| dayjs | npm | Choose a compact chainable API when plugins can supply the exact parsing and zone features you need. |
| moment | npm | Keep it in an established Moment codebase where migration risk exceeds the cost of its mutable legacy model. |
More utils guides
lru-cache · ajv · type-fest · p-limit · find-up · js-yaml · the whole shelf →
How this guide is made: grounded in the library's documentation, release notes, changelog, and issue history, on a fixed rubric — not a hands-on install of every release. The 50 most-downloaded entries are additionally install-verified in clean containers. Corrections: contact the desk.

