Skip to main content

Implementing Temporal, the new date/time API for JavaScript (and Rust!)

· 12 min read

Developing a JavaScript engine in Rust can seem like pretty daunting task to some. In order to demystify working on a feature and to go over what we've been working on implementing in Boa recently, we thought we'd write a post about implementing a JavaScript feature in Rust.

More specifically, this will be the first in a series of posts primarily about implementing the new date/time built-in: Temporal. We'll be going over general lessons and interesting design choices we've stumbled upon, as well as the crates supporting that implementation.

Why should you care? Well, we are not only implementing Temporal for JavaScript, but for Rust as well ... more on that in a bit.

First, an aside!

What even is Temporal?

Temporal is a modern API for handling date/time in a calendar and time zone aware manner that includes nine objects with over 200+ methods.

In JavaScript, Temporal is a global built-in namespace object that includes each of these nine built-ins:

  • Temporal.Now
  • Temporal.PlainDate
  • Temporal.PlainTime
  • Temporal.PlainDateTime
  • Temporal.ZonedDateTime
  • Temporal.Duration
  • Temporal.Instant
  • Temporal.PlainYearMonth
  • Temporal.PlainMonthDay

But to be honest, this post isn't meant to give an overview of Temporal and its general API. If Temporal is news to you and you are interested in learning more, feel free to check out the phenomenal MDN documentation.

Back on track

Being Boa a JavaScript engine / interpreter, developing a correct implementation of the ECMAScript specification is our raison d'être. This, in consequence, makes implementing Temporal one of our most important goals, since it represents roughly 7-8% of the current conformance test suite (~4000 of the ~50,000 tests).

When the PR of the first prototype of Temporal for Boa was submitted, a few things became evident:

  1. Date/Time is a complicated beast (duh)
  2. There's room for optimization and improvement
  3. This would be handy to have in Rust

So after the prototype was merged, we pulled it out of Boa's internal builtins and externalized it into its own crate, temporal_rs, which landed behind an experimental flag in Boa v0.18.

After over a year and a half of development, Boa now sits at a conformance of about 90% for Temporal (and growing), with the entire implementation being backed by temporal_rs.

For its part, temporal_rs is shaping up to be a proper Rust date/time library that can be used to implement Temporal in a JavaScript engine, and even support general date/time use cases.

Let's take a look at Temporal: it's JavaScript API, it's Rust API in temporal_rs, and how temporal_rs supports implementing the specification.

Important core differences

First, we need to talk about JavaScript values (JsValue) for a bit. This is functionally the core any value type of JavaScript. A JsValue could be a number represented as a 64 bit floating point, a string, a boolean, or an object. Not only is it an any, but JsValue is ultimately engine defined, with various implementations existing across engines.

While this is handy for a dynamically typed language like JavaScript, it is not ideal for implementing deep language specifications where an object or string may need to be cloned. Furthermore, it's just not great for an API in a typed language like Rust.

To work around this, we routinely use FromStr and a FiniteF64 custom primitive to handle casting and constraining, respectively, which glues dynamic types like JsValue with a typed API.

For instance, in Boa, we heavily lean into using the below patterns:

// (Note: this is abridged for readability)

// FiniteF64 usage
let number: f64 = js_value.to_number(context)?;
let finite_f64: FiniteF64 = FiniteF64::try_from(number)?;
let year: i32 = finite_f64.as_integer_with_truncation::<i32>();

// FromStr usage with `get_option`
let options_obj: &JsObject = get_options_object(&js_value)?;
let overflow: Option<ArithmeticOverflow> = get_option::<ArithmeticOverflow>(
&options_obj,
js_string!("overflow"),
context
)?;

This is the core glue between Boa and the temporal_rs API that we will be going over below.

Implementing constructors

There are a variety of ways to construct a core component like PlainDate, and that stems from the core constructor for each of the core components: new_with_overflow.

impl PlainDate {
pub fn new_with_overflow(year: i32, month: u8, day: u8, calendar: Calendar, overflow: ArithmeticOverflow) -> Result<Self> {
// Create PlainDate
}
}

This function supports the baseline construction of Temporal builtins, which takes the usual year, month, day, alongside a calendar and also an overflow option to constrain or reject based on whether the provided values are in an expected range.

However, we can better express this in Rust with common try_ prefix notation.

impl PlainDate {
pub fn new(year: i32, month: u8, day: u8, calendar: Calendar) -> Result<Self> {
Self::new_with_overflow(year, month, day, calendar, ArithmeticOverflow::Constrain)
}

pub fn try_new(year: i32, month: u8, day: u8, calendar: Calendar) -> Result<Self> {
Self::new_with_overflow(year, month, day, calendar, ArithmeticOverflow::Reject)
}
}

These three constructors, new_with_overflow, try_new, and new, are fairly flexible and provide full coverage of the Temporal specification.

For instance, take the below snippet:

const plainDate = new Temporal.PlainDate(2025, 6, 9);

This code can easily be translated to Rust as:

use temporal_rs::PlainDate;
let plain_date = PlainDate::try_new(2025, 6, 9, Calendar::default())?;

Furthermore, we actually learn some interesting things about the JavaScript API from looking at the temporal_rs API:

  1. The Temporal.PlainDate constructor can throw.
  2. When the calendar is omitted, the default calendar is used (this will default to the iso8601 calendar)

Of course, if you somewhat prefer the brevity of the JavaScript API and don't want to list the default Calendar, temporal_rs provides the additional constructors new_iso and try_new_iso.

use temporal_rs::PlainDate;
let plain_date = PlainDate::try_new_iso(2025, 6, 9)?;

Interestingly enough, the _iso constructors are mostly expressing a part of the JavaScript API, just in native Rust. This is because in JavaScript the _iso constructors are assumed to exist due to resolving an undefined calendar to the default ISO calendar.

Let's discuss Now

Colonel Sandurz: Now. You're looking at now, sir. Everything that happens now, is happening now.
Dark Helmet: What happened to then?
Colonel Sandurz: We passed then.
Dark Helmet: When?
Colonel Sandurz: Just now. We're at now now.
Dark Helmet: Go back to then.
Colonel Sandurz: When?
Dark Helmet: Now.
Colonel Sandurz: Now?
Dark Helmet: Now.
Colonel Sandurz: I can't.
Dark Helmet: Why?
Colonel Sandurz: We missed it.
Dark Helmet: When?
Colonel Sandurz: Just now.
Dark Helmet: When will then be now?
-- Spaceballs, 1987

Temporal.Now is an incredibly strange type, yet nevertheless important. It is the object from which the current instant can be measured and mapped into any of the Temporal components.

In JavaScript, this type has no [[Construct]] or [[Call]] internal method, which is a fancy way to say that Now has no constructor and cannot be called directly.

Instead, Now is used primarily as a namespace for its methods.

And this was reflected in early adaptions of Now, which looked more or less like the below:

struct Now;

impl Now {
pub fn instant() -> Instant;

pub fn zoned_date_time_iso() -> ZonedDateTime;
}

Interestingly enough, the above implementation is incorrect, or at the very least not ideal.

Hidden in the specification steps for Now are some very tricky steps invoking the abstract operations: SystemTimeZoneIdentifier and SystemUtcEpochNanoseconds. That's great, let's just use the usual suspects SystemTime and iana-time-zone, merge it, and call it a day on the implementation, right?

Except the core purpose of temporal_rs is that it can be used in any engine implementation, and accessing a system clock and system time zone is sometimes difficult for engines that support targets like embedded systems. Thus, this functionality must be delegated to the engine or runtime ... somehow.

How did we end up implementing Now if we have no access to the system clock or time zone? Well ... a builder pattern of course!

#[derive(Default)]
pub struct NowBuilder {
clock: Option<EpochNanoseconds>,
zone: Option<TimeZone>,
}

impl NowBuilder {
pub fn with_system_nanoseconds(mut self, nanoseconds: EpochNanoseconds) -> Self {
self.clock = Some(nanoseconds);
self
}

pub fn with_system_zone(mut self, zone: TimeZone) -> Self {
self.zone = Some(zone);
self
}

pub fn build(self) -> Now {
Now {
clock: self.clock,
zone: self.zone.unwrap_or_default(),
}
}
}

pub struct Now {
clock: Option<EpochNanoseconds>,
zone: TimeZone,
}

Once we've constructed Now, then we are off to the races!

To show the NowBuilder in action, in Boa, the implementation for Temporal.Now.plainDateISO() with the builder API is shown below:

impl Now {
// The `Temporal.Now.plainDateISO` used when building `Temporal.Now`.
fn plain_date_iso(_: &JsValue, args: &[JsValue], context: &mut Context) -> JsResult<JsValue> {
let time_zone = args
.get_or_undefined(0)
.map(|v| to_temporal_timezone_identifier(v, context))
.transpose()?;

let now = build_now(context)?;

let pd = now.plain_date_iso_with_provider(time_zone, context.tz_provider())?;
create_temporal_date(pd, None, context).map(Into::into)
}
}

// A helper for building Now
fn build_now(context: &mut Context) -> JsResult<NowInner> {
Ok(NowBuilder::default()
.with_system_zone(system_time_zone()?)
.with_system_nanoseconds(system_nanoseconds(context)?)
.build())
}

The nice part about this approach is that it also allows a std implementation that can be feature gated for general users that are not concerned with no_std.

    // Available with the `sys` feature flag
use temporal_rs::Temporal;
let now = Temporal::now().instant();

Partial API

There's an interesting method on each of the Temporal built-ins that I'd assume most people who have used Rust would be familiar with: from. But this isn't Rust's friendly From trait. No, this from is a behemoth method that takes a JsValue and automagically gives you back the built-in that you'd like or throws. That's right! Give it a string, give it a property bag, give it an instance of another Temporal built-in; from will figure it out for you!

Simple, right?

Folks, we're pleased to announce that temporal_rs won't be supporting that! ... or at least not in that shape.

Again, the goal of temporal_rs is to implement the specification to the highest possible degree of conformance, so when we couldn't provide a direct translation of the specification's API, we made sure to provide APIs that (hopefully) made the glue code between engines and temporal_rs much shorter.

To exemplify this, let's take a look at some valid uses of from in JavaScript to construct a PlainDate.

// Create a `PlainDateTime`
const pdt = new Temporal.PlainDateTime(2025, 1, 1);
// We can use the `PlainDateTime` (`ZonedDateTime` / `PlainDate` are also options).
const pd_from_pdt = Temporal.PlainDate.from(pdt);
// We can use a string.
const pd_from_string = Temporal.PlainDate.from("2025-01-01");
// We can use a property bag.
const pd_from_property_bag = Temporal.PlainDate.from({
year: 2025,
month: 1,
day: 1,
});

If we look closely to the common usage of the method, it seems like all that needs to be implemented by temporal_rs is:

  • From<PlainDateTime>: Easy.
  • From<ZonedDateTime>: Simple.
  • FromStr: Tricky but can be done.
  • From<JsObject>: ... ... oh. Did I mention JsObject, like JsValue, is engine defined as well?

Fortunately, this is where temporal_rs's Partial API comes in.

It turns out that, while property bags in JavaScript can have various fields set, there is still a general shape for the fields that can be provided and validated in Temporal.

To support this in temporal_rs, a "partial" component exists for each of the components that can then be provided to that component's from_partial method.

With this, we have fully implemented support for the from method in temporal_rs:

use core::str::FromStr;
use temporal_rs::{PlainDate, PlainDateTime, partial::PartialDate};
let pdt = PlainDateTime::try_new_iso(2025, 1, 1)?;
// We can use the `PlainDateTime` (`ZonedDateTime` / `PlainDate` are also options).
let pd_from_pdt = PlainDate::from(pdt);
// We can use a `str`.
let pd_from_string = PlainDate::from_str("2025-01-01")?;
// We can use a `PartialDate`.
let pd_from_partial = PlainDate::from_partial(
PartialDate::new()
.with_year(Some(2025))
.with_month(Some(1))
.with_day(Some(1))
);

NOTE: there may be updates to PartialDate in the future (see boa-dev/temporal #349 for more information).

Elephant in the room: time zones

So far we have not discussed time zones, and -- surprise! -- we aren't going to ... yet. It's not because they aren't super cool and interesting and everyone totally 100% loves them. No, time zones aren't in this post because they are still being polished and deserve an entire post of their own.

So stay tuned for our next post on implementing Temporal! The one where we'll hopefully go over everyone's favorite subject, time zones; and answer the question that some of you may have if you happen to take a glance at temporal_rs's docs or try out our no_std support: what in the world is a provider API?

Conclusion

In conclusion, we're implementing Temporal in Rust to support engine implementors as well as to have the API available in native Rust in general.

If you're interested in trying Temporal using Boa, you can use it in Boa's CLI or enable it in boa_engine with the experimental flag.

Outside of Boa's implementation, temporal_rs has implemented or supports the implementation for a large portion of the Temporal's API in native Rust. Furthermore, an overwhelming amount of the API can be considered stable1 and is currently available in Boa with only a few outstanding issues that may be considered breaking changes.

If you're interested in trying out temporal_rs, feel free to add it to your dependencies with the command:

cargo add temporal_rs

or by adding the below in the [dependencies] section of your Cargo.toml:

temporal_rs = "0.0.9"

A FFI version of temporal is also available for C and C++ via temporal_capi.

Footnotes

  1. A general note on API stability

    While the majority of the APIs discussed above are expected to be mostly stable, Temporal is still a stage 3 proposal that is not fully accepted into the ECMAScript specification. Any normative change that may be made upstream in the ECMAScript or ECMA402 specification will also be reflected in temporal_rs.

    There are also a few outstanding issues with changes that may be reflected in the API.

    1. Duration's inner repr and related constructors.
    2. ZonedDateTime.prototype.getTimeZoneTransition implementation
    3. TemporalError's inner repr
    4. Partial objects may need some adjustments to handle differences between from_partial and with
    5. Time zone provider's and the TimeZoneProvider trait are still largely unstable. Although, the provider APIs that use them are expected to be stable (spoilers!)
    6. Era and month code are still be discussed in the intl-era-month-code proposal, so some calendars and calendar methods may have varying levels of support.

    The above issues are considered blocking for a 0.1.0 release.

How ECMAScript Engines Optimize Your Variables

· 14 min read

In this post, we will dive into how ECMAScript engines store variables, go over storage optimizations, and learn about scope analysis. If you are an ECMAScript developer, you will get some practical tips to improve the performance of your code. If you write your own ECMAScript engine or any interpreter/compiler, you might get some implementation ideas.

Boa release v0.20

· 8 min read

Summary

Boa v0.20 is now available! After 5 months of development we are very happy to present you the latest release of the Boa JavaScript engine. Boa makes it easy to embed a JS engine in your projects, and you can even use it from WebAssembly. See the about page for more info.

In this release, our conformance has grown from 87.3% to 89.92% in the official ECMAScript Test Suite (Test262). This small jump is expected as we're shifting most of our focus to performance as the majority of the engine is now conformant. We will continue to implement more of the specification as we go along but we expect these changes to be much smaller than we've been used to.

You can check the full list of changes [here][changelog], and the full information on conformance [here][conformance].

Boa release v0.19

· 9 min read

Summary

Boa v0.19 is now available! After 4 months of development we are very happy to present you the latest release of the Boa JavaScript engine. Boa makes it easy to embed a JS engine in your projects, and you can even use it from WebAssembly. See the about page for more info.

In this release, our conformance has grown from 85.03% to 87.3% in the official ECMAScript Test Suite (Test262). Interestingly, this was partly because around 2000 tests were removed from test262 as part of the removal of custom calendars and timezones. Overall, Boa's conformance in real terms has improved by ~6%.

You can check the full list of changes [here][changelog], and the full information on conformance [here][conformance].

Boa release v0.18

· 20 min read

Summary

Boa v0.18 is now available! After 7 months of development we are very happy to present you the latest release of the Boa JavaScript engine. Boa makes it easy to embed a JS engine in your projects, and you can even use it from WebAssembly. See the about page for more info.

In this release, our conformance has grown from 79.36% to 85.03% in the official ECMAScript Test Suite (Test262). This means we now pass 3,550 more tests than in the previous version. Moreover, our amount of ignored tests decreased from 9,496 to 1,391 thanks to all the new builtins we have implemented for this release.

You can check the full list of changes here, and the full information on conformance here.

You probably noticed that something seems different... This release marks a major update to the design of our website, and the introduction of our new logo! We'd like to thank @ZackMitkin for being the one that started the work on this nifty redesign, and @kelbazz for designing the logo. We're planning to add some additional pages to learn more about the APIs that Boa exposes. Additionally, expect some more blog posts from us in the future! We would like to write about how to use certain APIs, design challenges that we encountered while developing the engine, and internal implementation details. Subscribe to our RSS feed if you're interested in staying up to date!

This big release was partly possible thanks to those who have supported us. Thanks to funds we've received we have been able to renew our domain name, remunerate members of the team who have worked on the features released, and discuss the possibility of using dedicated servers for benchmarking. If you wish to sponsor Boa, you can do so by donating to our open collective. You can also check easy or good first issues if you want to contribute some code instead.

Boa release v0.17

· 12 min read

Summary

Boa v0.17 is now available! This is one of the biggest Boa releases since the project started, and after around 7 months of development, we are very happy to present you the latest release of the Boa JavaScript engine. Boa makes it easy to embed a JS engine in your projects, and you can even use it from WebAssembly. See the about page for more info.

In this release, our conformance has grown from 74.53% to 78.74% in the official ECMAScript Test Suite (Test262). While this might look as a small increase, we now pass 6,079 more tests than in the previous version. In any case, the big changes in this release are not related to conformance, but to huge internal enhancements and new APIs that you will be able to use.

You can check the full list of changes here, and the full information on conformance here.

Moreover, this big release was partly possible thanks to a grant by Lit Protocol. Thanks to this grant, we were able to remunerate 2 team members for their 20h/week work each during three and a half months. If you wish to sponsor Boa, you can do so by donating to our open collective. You can also check easy or good first issues.

Furthermore, we now have a new domain for Boa, boajs.dev.

Adding a JavaScript interpreter to your Rust project

· 12 min read

Introduction

When we develop tools for our users, we sometimes want to give them some form of control over how they work. This is common in games, where we can add scripting for our users to be able to create extensions, or even for business tools, where we allow our customer to change or extend the behaviour of our platform. For those cases, using Rust, a compiled, type safe language can be a challenge, since once a program has been compiled, it's tricky to change or extend it at runtime. Furthermore, many of our users will prefer to use a more common scripting language, such as JavaScript.

This is where Boa enters the scene. Boa is a Javascript engine fully written in Rust. Currently, it can be used in places where you need most of the JavaScript language to work, even though, we would advise to wait to get all our known blocker bugs solved before using this for critical workloads. You can check how conformant we are with the official ECMAScript specification here.

And, before going further, we would like to mention that you can contribute to Boa by solving one of the issues where we need special help, and we now also accept financial contributions in our OpenCollective page.

Note: You can see more examples of integrating Boa in our repository.

Boa release v0.16

· 4 min read

Summary

Boa v0.16 is now available! After around 3 months of development, we are very happy to present you the newest release of the Boa JavaScript engine. Boa makes it easy to embed a JS engine in your projects, and you can even use it from WebAssembly. See the about page for more info.

Boa currently supports part of the JavaScript language. In this release, our conformance has grown from 62.29% to 74.53% in the official ECMAScript Test Suite (Test262). The engine now passes 68,612 tests, coming from 56,372 in Boa 0.15 (21.7% increase), and we have closed 9 issues and merged 59 pull requests. You can check the full list of changes here, and the full information on conformance here.

Boa release v0.15

· 5 min read

Summary

Boa v0.15 is now available! After around 3 months of development, we are very happy to present you the newest release of the Boa JavaScript engine. Boa makes it easy to embed a JS engine in your projects, and you can even use it from WebAssembly. See the about page for more info.

Boa currently supports part of the JavaScript language. In this release, our conformance has grown from 49.74% to 62.29% in the official ECMAScript Test Suite (Test262). The engine now passes 56,372 tests, coming from 43,986 in Boa 0.14 (28.1% increase), and we have closed 18 issues and merged 58 pull requests. You can check the full list of changes here, and the full information on conformance here.

Boa release v0.14

· 6 min read

Summary

Boa v0.14 is here! After almost 6 months of development, we are very happy to present you the newest release of the Boa JavaScript engine. Boa makes it easy to embed a JS engine in your projects, and you can even use it from webassembly. See the about page for more info. Together with this release, we present you: A new way to contribute to Boa, a virtual machine, usable examples and much more.

Boa currently supports part of the JavaScript language. In this release, our conformance has grown from 41.01% to 49.74% in the official ECMAScript Test Suite (Test262). The engine now passes 43,986 tests, coming from 33,192 in Boa 0.13 (32.5% increase), and we have closed 40 issues and merged 137 pull requests. You can check the full list of changes here, and the full information on conformance here.

Boa release v0.13

· 5 min read

Boa v0.13 is here! Boa is a JavaScript engine written in the Rust programming language. It makes it easy to embed a JS engine in your projects, and you can even use it from webassembly. See the about page for more info.

We currently support part of the language. In this release, our conformance has grown to 41.97% of the official ECMAScript Test Suite (Test262). We have closed 40 issues and merged 105 pull requests. You can check the full list of changes here.

This release brings some new features, such as support for calling Rust closures from JavaScript to improve better interopability between JS and Rust.

Boa release v0.12

· 3 min read

Boa v0.12 is here! Boa is a JavaScript parser, compiler and executor written in the Rust programming language. It makes it easy to embed a JS engine in your projects, and you can even use it from webassembly. See the About page for more info.

We currently support part of the language. In this release, our conformance has grown to 33.97% of the official ECMAScript Test Suite (Test262). In this release, we have closed 19 issues and merged 69 pull requests. You can check the full list of changes here.

Let's dive into the most relevant changes of this release.

Boa release v0.11

· 5 min read

Boa has reached a new release. v0.11, our biggest one yet!

Since v0.10 we've closed 77 issues and merged 129 pull requests. The engine has been faster and more compliant to the spec. Below are some of the highlights but please see the changelog for more information.

What is Boa? See the About page for more info.

Boa release v0.10

· 4 min read

Boa is an experimental Javascript lexer, parser and compiler written in Rust. It has support for some of the language, can be embedded in Rust projects fairly easily and also used from the command line. Boa also exists to serve as a Rust implementation of the EcmaScript specification, there will be areas where we can utilise Rust and its fantastic ecosystem to make a fast, concurrent and safe engine.

We have a long way to go, however v0.10 has been the biggest release to date, with 138 issues closed!

We have some highlights, but if you prefer to read the full changelog, you can do that here

Boa v0.9: measureme & optimisations

· 6 min read

Hello World!

Boa is an experimental Javascript lexer, parser and compiler written in Rust. It has support for some of the language, can be embedded in Rust projects fairly easily and also used from the command line.
Boa also exists to serve as a Rust implementation of the EcmaScript specification, there will be areas where we can utilise Rust and its fantastic ecosystem to make a fast, concurrent and safe engine.

Today we're pleased to announce our latest release, version 0.9.
v0.9 is by far the biggest release we've had since Boa began. You can find the full changes from the changelog. The milestone behind this version was further optimisation and an increase in new features. We can show you how we can identify areas that can be optimised.