Two of the most anticipated JavaScript features are close to working in every major browser. The Safari 27.3 beta adds Temporal, the modern date and time API, and using declarations for automatic resource cleanup. Both already ship in current Chrome and Firefox, according to Can I use, so once Safari 27.3 reaches stable you can rely on them across the big three engines. This guide gives you a practical tour of both features, with code you can paste into a browser console today and advice on shipping them safely while older Safari versions are still around.

What landed in Safari 27.3 beta

Apple’s Safari 27.3 release notes and MDN’s browser-compat-data update for Safari 27.3 beta list a long set of new features, including Temporal, using, CSS @function, corner-shape, popover="hint" and dialog closedby. This post covers the two JavaScript language features. Safari 27.3 is still in beta, so Safari 27.2 and earlier don’t support either one.

Part 1: Temporal, dates without the pain

The old Date object has well-known problems. It’s mutable, months start at 0, parsing is inconsistent and it only knows UTC and the user’s local time zone. Temporal fixes this with separate immutable types for different jobs.

Type Use it for Example
Temporal.PlainDate A calendar date with no time or zone A birthday, a due date
Temporal.PlainTime A wall-clock time Shop opens at 09:30
Temporal.PlainDateTime Date and time, no zone A form value before you know the zone
Temporal.ZonedDateTime An exact moment in a named time zone A meeting at 21:30 in Asia/Kolkata
Temporal.Instant An exact moment, no calendar Log timestamps
Temporal.Duration A length of time 1 hour 30 minutes

Getting “now”

const today = Temporal.Now.plainDateISO();           // e.g. 2026-11-14
const nowIST = Temporal.Now.zonedDateTimeISO("Asia/Kolkata");
const instant = Temporal.Now.instant();               // exact UTC moment

console.log(today.toString());
console.log(nowIST.toString()); // 2026-11-14T21:30:00+05:30[Asia/Kolkata]

Date arithmetic that does what you mean

const start = Temporal.PlainDate.from("2026-01-31");

console.log(start.add({ months: 1 }).toString()); // 2026-02-28
console.log(start.add({ days: 30 }).toString());  // 2026-03-02

Adding a month to 31 January gives 28 February, because by default Temporal constrains the result to the last valid day instead of spilling into March. Every operation returns a new object, so start never changes.

Differences between dates

const joined = Temporal.PlainDate.from("2026-08-01");
const deadline = Temporal.PlainDate.from("2026-12-25");

const gap = joined.until(deadline, { largestUnit: "days" });
console.log(gap.days); // 146

const human = joined.until(deadline, { largestUnit: "months" });
console.log(human.toString()); // P4M24D (4 months, 24 days)

Time zones without a library

const meeting = Temporal.ZonedDateTime.from(
  "2026-11-20T21:30[Asia/Kolkata]"
);

const forNewYork = meeting.withTimeZone("America/New_York");
console.log(forNewYork.toPlainTime().toString()); // 11:00:00

console.log(
  meeting.toLocaleString("en-IN", { dateStyle: "medium", timeStyle: "short" })
);

ZonedDateTime also handles daylight saving time correctly. Adding { days: 1 } keeps the same wall-clock time even when a DST change happens overnight, while adding { hours: 24 } adds exactly 24 hours.

Durations

const video = Temporal.Duration.from({ hours: 1, minutes: 30 });
console.log(video.total("minutes")); // 90

const sorted = ["2026-12-01", "2026-03-15", "2026-07-04"]
  .map((d) => Temporal.PlainDate.from(d))
  .sort(Temporal.PlainDate.compare);
console.log(sorted.map(String)); // ["2026-03-15", "2026-07-04", "2026-12-01"]

Mixing with legacy Date

You will still receive Date objects from older libraries. Convert through epoch milliseconds:

const legacy = new Date();
const asInstant = Temporal.Instant.fromEpochMilliseconds(legacy.getTime());
const backToDate = new Date(asInstant.epochMilliseconds);

Part 2: using, automatic cleanup

Explicit resource management adds two declarations, using and await using. When the block that declares them ends, for any reason including a thrown error, JavaScript calls the resource’s [Symbol.dispose]() or [Symbol.asyncDispose]() method. Think of it as a built-in try/finally that you can’t forget.

A synchronous example

function startTimer(label) {
  const t0 = performance.now();
  return {
    [Symbol.dispose]() {
      console.log(`${label}: ${(performance.now() - t0).toFixed(1)} ms`);
    },
  };
}

function buildReport(rows) {
  using timer = startTimer("buildReport");
  if (!rows.length) throw new Error("No data"); // timer still logs
  return rows.map((r) => r.total).reduce((a, b) => a + b, 0);
}

Whether buildReport returns or throws, the timer logs its duration.

An async example: locks and connections

async function withLock(name) {
  let release;
  const acquired = new Promise((resolve) => {
    navigator.locks.request(name, () => new Promise((r) => {
      release = r;
      resolve();
    }));
  });
  await acquired;
  return { [Symbol.asyncDispose]: async () => release() };
}

async function saveDraft(draft) {
  await using lock = await withLock("draft-save");
  await fetch("/api/drafts", { method: "POST", body: JSON.stringify(draft) });
} // lock released here, even if fetch throws

Managing several resources with DisposableStack

When you open resources conditionally or in a loop, use DisposableStack (or AsyncDisposableStack). Resources are disposed in reverse order:

function openAll(urls) {
  using stack = new DisposableStack();
  const sockets = urls.map((u) => {
    const ws = new WebSocket(u);
    stack.defer(() => ws.close());
    return ws;
  });
  // If anything above threw, every socket opened so far is closed.
  const owned = stack.move(); // hand ownership to the caller
  return { sockets, [Symbol.dispose]: () => owned.dispose() };
}

defer() registers a cleanup callback, use() adds an object that already has [Symbol.dispose], adopt() wraps a value with a custom disposer, and move() transfers everything to a new stack so the current block doesn’t dispose it.

Shipping today: feature detection and fallbacks

Until Safari 27.3 is stable and most of your users have updated, plan for browsers without these features.

Temporal

Temporal is a runtime API, so you can detect it and load a polyfill only when needed:

if (typeof globalThis.Temporal === "undefined") {
  const { Temporal } = await import("@js-temporal/polyfill");
  globalThis.Temporal = Temporal;
}

using

using is syntax, so a browser that doesn’t understand it fails to parse the whole file. Runtime detection won’t help. Your options are:

  • Compile it away with TypeScript or Babel for older targets, and add a polyfill for Symbol.dispose, Symbol.asyncDispose and DisposableStack.
  • Serve modern and legacy bundles separately.
  • Use using freely in Node.js or Deno code, where you control the runtime version.
// Safe runtime check for the supporting APIs (not the syntax)
const hasDisposables =
  typeof Symbol.dispose === "symbol" && typeof DisposableStack === "function";

Testing in Safari

Install the Safari 27.3 beta (or Safari Technology Preview) and run the snippets above in the console. Watch WebKit’s release notes for the stable release, then check your analytics to see how quickly your Safari users update before you drop the polyfill.

Key takeaways

  • Safari 27.3 beta adds Temporal and using. Chrome and Firefox already ship both.
  • Use Temporal’s separate types (PlainDate, ZonedDateTime, Instant, Duration) instead of Date for clear, immutable date logic.
  • using and await using guarantee cleanup through [Symbol.dispose] and [Symbol.asyncDispose]. DisposableStack handles groups of resources.
  • Polyfill Temporal by feature detection. Transpile using, because it’s syntax.

Sources: Safari 27.3 release notes, MDN BCD: Updates for Safari 27.3 beta, MDN: Temporal and MDN: using.