@js-joda/core
@js-joda/core is an immutable date and time model patterned after Java's java.time and the ThreeTen backport. Instead of forcing every value into JavaScript Date, it separates calendar dates, wall-clock times, local date-times, offsets, instants, date-based periods, and time-based durations. ISO parsing and formatting are built in, and calculations return new values. Core handles ISO chronology plus fixed offsets and the system zone; named IANA zones and localized text live in separate js-joda packages.
@js-joda/core is an excellent fit when date-only values, instants, and calendar arithmetic must remain distinct and a java.time-style API is welcome. Skip it for light Date formatting or if separate zone and locale data packages feel heavier than your problem.
Use it if
- Your domain distinguishes a birthday or billing date from a timestamp and you want types that enforce that distinction
- A Java or Kotlin team wants JavaScript date code with familiar java.time concepts and method names
- You need immutable calendar arithmetic, ISO parsing, periods, durations, adjusters, and nanosecond-capable instants
- The project needs deterministic clock injection for date-sensitive tests without wrapping every call to now
- You want a small set of formatting helpers around native Date: the Java-inspired class graph, temporal fields, units, queries, and adjusters impose a real learning curve
- Named time zones must work from core alone: the README requires the separate @js-joda/timezone side-effect package to register IANA zone rules
- Localized month and weekday text is central: that requires @js-joda/locale plus locale data packages, and version 6 changed the recommended Locale import path
- You support Internet Explorer 11: the 6.0 changelog explicitly removed IE11 support
- You need non-ISO calendar systems or permissive human date parsing: the project describes an ISO, proleptic-Gregorian model and its formatters expect declared patterns rather than natural language
Setup reality
npm install @js-joda/core is straightforward: version 6.1.0 has no runtime or peer dependencies, ships CommonJS and ESM builds, and includes TypeScript declarations through its typings entry. The complexity comes from choosing the right domain type. LocalDate has no time or zone, LocalDateTime is only a wall-clock description, Instant is a point on the timeline, Period performs calendar math, and Duration measures elapsed time. Converting a local value to an instant always requires a zone or offset; do not silently use the machine zone for stored business data. Core does not bundle IANA rules. Install @js-joda/timezone and import it for side effects before ZoneId.of('Europe/Berlin') or other named zones; that package carries the changing tz database and offers reduced-range builds with explicit date limits. Localized formatting similarly needs @js-joda/locale and a locale data package. Since core 6, import Locale from @js-joda/locale, then import packages such as @js-joda/locale_en only to register data. The 6.0 release also simplified nativeJs and dropped IE11. Objects are immutable, so plusDays and withMonth return replacements rather than changing the original. Parsing errors throw DateTimeParseException, invalid calendar values throw DateTimeException, and Java-style formatter symbols have semantics worth testing, especially year-of-era versus proleptic year. native Date uses milliseconds while Instant can represent finer precision, so round trips through Date lose sub-millisecond data.
Patterns
Parse and serialize a date-only valueparse-local-date
import { LocalDate } from '@js-joda/core';
const birthday = LocalDate.parse('1990-05-23');
console.log(birthday.year(), birthday.monthValue(), birthday.dayOfMonth());
console.log(birthday.toString()); // 1990-05-23LocalDate intentionally has no time or zone, which makes it safer for birthdays, holidays, and billing dates.
Construct a validated calendar datecreate-local-date
import { LocalDate } from '@js-joda/core';
const release = LocalDate.of(2026, 8, 8);Invalid dates such as February 30 throw DateTimeException instead of rolling into March like native Date can.
Add months and days immutablyadd-calendar-time
const start = LocalDate.parse('2026-01-31');
const renewed = start.plusMonths(1).plusDays(3);
console.log(start.toString()); // 2026-01-31
console.log(renewed.toString()); // 2026-03-03plusMonths resolves an invalid target day to the month's last valid day. Every operation returns a new value.
Order date-only valuescompare-local-dates
const today = LocalDate.parse('2026-08-08');
const deadline = LocalDate.parse('2026-08-31');
if (today.isBefore(deadline)) {
console.log('still open');
}Use isBefore, isAfter, isEqual, or compareTo. JavaScript < on these objects is not the intended API.
Calculate calendar and total-day differencesmeasure-date-difference
import { ChronoUnit, LocalDate, Period } from '@js-joda/core';
const start = LocalDate.parse('2025-12-31');
const end = LocalDate.parse('2026-03-02');
const calendar = Period.between(start, end);
const days = start.until(end, ChronoUnit.DAYS);Period is expressed in calendar years, months, and days; ChronoUnit.DAYS gives a total date-line day count.
Model a wall-clock appointmentparse-local-date-time
import { LocalDateTime } from '@js-joda/core';
const appointment = LocalDateTime.parse('2026-10-25T09:30:00');
const reminder = appointment.minusMinutes(30);LocalDateTime has no offset or zone and is not an instant. Attach an explicit zone before comparing it with timestamps.
Parse an absolute timestampparse-instant
import { Instant } from '@js-joda/core';
const receivedAt = Instant.parse('2026-08-08T12:30:45.123Z');
console.log(receivedAt.toEpochMilli());Version 6.1 accepts ISO timestamps with offsets and resolves them to UTC. Instant can retain nanoseconds, while epoch milliseconds cannot.
Convert to and from JavaScript Dateconvert-native-date
import { convert, nativeJs } from '@js-joda/core';
const zoned = nativeJs(new Date('2026-08-08T12:30:00Z'));
const nativeDate = convert(zoned).toDate();nativeJs returns a ZonedDateTime. Native Date stores milliseconds, so any finer Instant precision is lost on conversion.
Parse and format with an explicit patternformat-custom-pattern
import { DateTimeFormatter, LocalDate } from '@js-joda/core';
const formatter = DateTimeFormatter.ofPattern('dd/MM/uuuu');
const date = LocalDate.parse('08/08/2026', formatter);
console.log(date.format(formatter));Core patterns handle numeric ISO fields. Localized names require @js-joda/locale plus registered locale data.
Find the last day of a monthadjust-calendar-date
import { LocalDate, TemporalAdjusters } from '@js-joda/core';
const date = LocalDate.parse('2026-02-10');
const monthEnd = date.with(TemporalAdjusters.lastDayOfMonth());
console.log(monthEnd.toString()); // 2026-02-28Adjusters return new temporal values and encode calendar intent more clearly than manual day arithmetic.
Attach a named IANA time zoneuse-iana-timezone
import { LocalDateTime, ZoneId } from '@js-joda/core';
import '@js-joda/timezone';
const berlin = LocalDateTime.parse('2026-07-10T09:00')
.atZone(ZoneId.of('Europe/Berlin'));Install and import @js-joda/timezone for side effects before using named zones. Core alone supplies fixed offsets and the system zone.
Inject a fixed clock in testsfreeze-current-time
import { Clock, Instant, LocalDate, ZoneOffset } from '@js-joda/core';
const clock = Clock.fixed(
Instant.parse('2026-08-08T23:30:00Z'),
ZoneOffset.UTC
);
const today = LocalDate.now(clock);Passing Clock avoids global timer mocks and makes the zone governing today explicit.
Alternatives
| Package | Registry | Pick it when |
|---|---|---|
| date-fns | npm | You prefer tree-shakable functions around native Date and need a broad menu of formatting and calendar helpers |
| luxon | npm | You want immutable date-times with Intl-backed zones and locales in a more JavaScript-shaped API |
| dayjs | npm | You want a compact Moment-like chainable API and can add capabilities through plugins |
| temporal-polyfill | npm | You want the JavaScript Temporal model and a migration path toward the standard API |