PlainYearMonth is Weird
After nearly a decade of waiting, Temporal finally reached Stage 4 last week. Getting here was a monumentous task by many very smart people to fix difficult and complex issues with the web platform.
There are a lot of useful features that I'd like to take the time to write about at some point in the future.
Instead of doing that, I'm going to complain about some weirdness.
PlainYearMonth is weird.
Or maybe PlainYearMonth.prototype.toLocaleString is weird?
Or is it that DateTimeFormat handles PlainYearMonth in a werid way?
I'm not sure which. It's weird.
To provide some context, if you create any of the various Temporal types that include full dates (PlainDate, PlainDateTime, ZonedDateTime) you can use DateTimeFormat to format that date with locale and formatting options. Even better, you can use the toLocaleString method as a fast-and-easy way to format the date:
Temporal.Now.plainDateISO().toLocaleString("en", {
year: "numeric",
month: "long",
day: "numeric",
});
// March 19, 2026
Where it gets weird is that PlainYearMonth is inconsistent.
If I'm creating a calendar for a given month, I should use a Temporal.PlainYearMonth object to represent the month. I should then be able to format the month and year like I would for PlainDate.
If I change the previous example to just format the year and month, I get the result that I'd expect:
Temporal.Now.plainDateISO().toLocaleString("en", {
year: "numeric",
month: "long",
});
// March 2026
But if I convert from PlainDate to PlainYearMonth things go weird:
Temporal.Now.plainDateISO().toPlainYearMonth().toLocaleString("en", {
year: "numeric",
month: "long",
});
// Uncaught RangeError: Mismatched calendars.
Wat?!
For some reason toLocaleString or DateTimeFormat suddenly needs a calendar option specified.
Do you know where you can get the calendar value?
PlainYearMonth.prototype.calendarId.
Why should I have to specify the calendar if DateTimeFormat already has access to the calendarId?
Could I maybe specify a different calendar when formatting the PlainYearMonth?
Temporal.Now.plainDateISO().toPlainYearMonth().toLocaleString("en", {
calendar: "hebrew",
year: "numeric",
month: "long",
});
// Uncaught RangeError: Mismatched calendars.
I guess not.
So to make this work I guess I'd better use a temp variable.
So let's do that and get our expected result of "March 2026".
const entirelyUnnecessaryVariable =
Temporal.Now.plainDateISO().toPlainYearMonth();
entirelyUnnecessaryVariable.toLocaleString("en", {
calendar: entirelyUnnecessaryVariable.calendarId,
year: "numeric",
month: "long",
});
// 2026 March

So yea, I guess don't use DateTimeFormat or toLocaleString with PlainYearMonth because it's broken*, and there's a good chance that it can never be fixed now.
If you're looking for a work-around, convert it to a PlainDate before formatting:
Temporal.PlainYearMonth.from("2026-03")
.toPlainDate({ day: 1 })
.toLocaleString("en", {
year: "numeric",
month: "long",
});
// March 2026
* It's probably not broken. There's probably some obscure and complex reason that the "obviously correct" behavior is ambiguous or Bad Actually™, but since I don't know the reason I'm going to just call it broken instead and let someone else tell me how I'm wrong.
