tc39

Activity

Stars and mentions · Last 30 days, Oct 7 – Nov 5

GitHub stars42
Mentions24
02468Oct 7Oct 21Nov 5
UTC days · repositories tc39 owns6 repositories

Top repositories

tc39’s repositories ranked by pull request authors, as on trending.

Loading

Contributes to

Repositories outside tc39’s account where tc39 opened or reviewed pull requests in the past 365 days.

Who noticed tc39

Everyone who starred or forked tc39’s repositories since the archive began in 2011.

Notable accounts

Ordered by the stars their own repositories have received.

Biggest fans

Accounts that starred the most of these repositories.

Breakout weeks

Each repository’s best week of new stars, with the release published around it.

Mentions

26posts
Nov 62 posts
  1. Web 標準動向 2026年10月版 こんにちは! サイボウズ株式会社 プロダクトエンジニアの くらっち (@kuracchi04) です。 はじめに サイボウズは 2025 年 4 月より、W3C のメンバーに加入しました。 blog.cybozu.io 標準化プロセスに関わることができるようになるための最初の一歩として、フロントエンドエンジニアの一部のメンバーは積極的に Web 標準のキャッチアップを行っています。 そこで、毎月メンバーが興味を持った Web 標準に関する話題や、実際に標準化プロセスに関わることができた場合にはその報告などを 1 つの記事としてまとめ、紹介していきます。 また、ここでは W3C に限らず、TC39 や WHATWG などの標準化団体のトピックについても扱います。 ! 今月の執筆者は以下の 4 名です。 nus3 State of系サーベイへのスポンサーのお知らせを執筆 Saji ECMA402 (Intl) 周りのトピックを執筆 saku HTML, CSS に関連するトピックを執筆 mehm8128 主にアクセシビリティに関連するトピックを執筆 くらっち ECMA262 周りのトピックを執筆 State of系サーベイへのスポンサー State of JS などを運営されている Sacha Greif さんのスポンサーを、弊社でさせていただくことになりました! devographics.com State of 系のサーベイでは、開発者が実際にどの機能やライブラリを利用していて、それらをどう感じているのかを知ることができます。サイボウズでも、プロダクト開発の際に参考にさせてもらっています。 State of 系のサイトにメールアドレスを登録しておくと、サーベイの開始時期をメールで知らせてくれます。結果を見るだけでなく、普段プロダクト開発をしている方はぜひサーベイに参加してみてください。 回答していく中で、この 1 年で追加された機能を振り返ることができたり、知らなかった機能に出会えたりするのでおすすめです。 HTML Updated Candidate Recommendation: Scalable Vector Graphics (SVG) 2 w3.org SVG2 の Candidate Recommendation Snapshot が更新されました。 2026/12/1 までコメントを募集しているとのことです。 Blink: Intent to Prototype: The focusreset event groups.google.com 要素が消えてキーボードフォーカスがリセットされたときに発火する、focusreset イベントの Intent to Prototype が出ました。 Chromium では要素が削除されたときに blur イベントが発火しますが、Gecko と WebKit では発火しないため、新たにイベントを用意し、Chromium でも要素が削除されたときにはこちらのイベントを発火するようになる予定とのことです。 これは、UI ライブラリで hacky な方法を使ってフォーカス制御している utils などを置き換えるのに役に立ちます。 Blink: Intent to Prototype: document.advanceFocus() and beforefocus groups.google.com element.focus() はキーボードフォーカスの移動先が分かっている場合に移動先要素のメソッドを利用して発火します。しかし、Tab キーを押したときのように、ブラウザが次の移動先を選択してキーボードフォーカスを移動させたいときのメソッドがありませんでした。 そこで現在 Open UI で議論中である document.advanceFocus() や beforefocus を用いて、移動先要素の計算をブラウザに委ねて「次のフォーカス可能要素にキーボードフォーカスを進める」ということを命令するメソッドが開発されるようです。 focusgroup 属性よりも低レベルで命令的なメソッドになっています。 併せて、要素がフォーカス可能かどうかを紹介する API の案も出されています。 Proposal: Expose sequential focus navigation primitives · Issue #13051 · whatwg/html Gecko: Intent to prototype: Retain focus when elements become disabled groups.google.com button などの要素にキーボードフォーカスしている最中にその要素が disabled になったとき、キーボードフォーカスを失わないようにする変更のプロトタイプが作成されています。 今まではキーボードフォーカスを失っていたため、disabled 属性の代わりに aria-disabled などを用いて無効化状態を実装する hack が用いられることがありましたが、そのような対応が不要になります。 なお、一度フォーカスを外して再度戻ったときには、今まで通りフォーカスが当たらないようです。 Should focused elements which become disabled lose focus? · Issue #13159 · whatwg/html Ship: HTML-in-canvas groups.google.com Chrome の 157 から Stable で HTML-in-Canvas が Ship されます。Stable での Ship はブラウザの中では初めてとなります。 これまで Canvas では、テキスト選択やページ内検索、翻訳、Autofill、アクセシビリティが失われ、逆に HTML を重ねると 3D 空間での前後関係やフレーム同期ズレ、毎フレームの CSS 再計算が課題となっていました。 <canvas content="drawable"> 内の要素を drawElementImage() で描画すれば、ブラウザ標準機能や AI エージェントと親和性の高いネイティブ HTML の機能を保ったままシェーダー表現を利用可能です。 Three.js や Babylon.js、PlayCanvas、PixiJS でも対応が進んでいます。 HTML-in-Canvas in Chrome 157: Interactive DOM inside 2D, WebGL, and WebGPU Overscroll areas – 28 October 2026 w3.org サイドメニューやボトムシートを、スクロールの仕組みを使って宣言的に実装する Overscroll Areas の設計が議論されています。2025 年の TPAC では HTML と CSS のどちらで扱うかが論点でしたが、現在は HTML の属性と Invoker Commands で構造と操作を表し、CSS で表示方法を制御する方向で進んでいます。scroll chaining、scroll snap、inert、backdrop、Invoker Commands など、既存の仕組みを組み合わせる設計です。 また、ボタンを必須とすることで、スワイプ操作を使えないキーボードや支援技術の利用者にも操作手段を提供可能にする点も決まっています。ボタンのデザインを前提としたスワイプデザインは、デザイナーにもコミュニケーションが必要になる部分かもしれません。 Overscroll Areas - TPAC 2026 whatwg/spec-ai: AI tooling for working with WHATWG standards whatwg/spec-ai WHATWG が、仕様の執筆やレビューを AI agent で支援するための repository を公開・運用しています。 HTML Living Standard の Pull Request をレビューし、問題の修正まで行う review-and-fix Skill や、WHATWG の記法を確認する静的チェックが追加されています。Pull Request 増加へのレビューを支援する Skill という位置付けです。 Custom Attributes webplatformco/project-custom-attributes 既存の HTML、SVG、MathML 要素へ、再利用可能な振る舞いを属性として追加する Custom Attributes が提案されています。要素自体を Custom Element に置き換えるのではなく、カスタムの属性で振る舞いのみを付与する発想です。 Customized built-in elements は、<button is="..."> のように is 属性を使って既存要素を拡張します。Interop 2027 では、相互運用が進んでいない customized built-in elements(is 属性)を非推奨化する提案も出ています。一方、Custom Attributes は要素の種類を保ったまま、複数の独立した振る舞いを属性として組み合わせられることを目指しています。 TPAC での議論も前向きに進んでおり、今後注目していきたい機能の一つです。 CSS [css-logical] Flow-relative syntax for margin-like shorthands csswg-drafts#1282 長年要望の多かった、Logical Properties のショートハンド対応方法が決定されました。 CSS には margin-block-start や margin-inline-end といった logical longhand がありますが、margin: 10px 20px 15px のような shorthand の値は、物理的な top、right、bottom、left の順序で展開されます。shorthand は parse 時に longhand へ展開されるのですが、logical property を物理方向へ対応付けるには、後から確定する writing-mode や direction が必要です。この処理順の違いが、logical shorthand の導入を難しくしてきました。 解決策として、パース時に Logical Properties であることをフラグとしてマークする方法が議論されました。今回の resolution では、値の末尾へ !logical または !physical を付ける形式と、複数の宣言を @logical または @physical で囲む形式を仕様化する予定です。 .box { margin: 10px 20px 15px !logical; } @logical { .box { margin: 10px 20px; padding: 5px; } } CSSWG TPAC misc w3.org w3.org [css-fonts][mediaqueries-5] Add prefers-bold-text user preference media feature csswg-drafts#14477 2026年9月の記事で紹介した prefers-bold-text media query を Media Queries に追加することが決まりました。 [css-mixins] Types for private properties (in mixins and elsewhere) and probably function locals csswg-drafts#14442 @private と @function の本文内に @property をネストし、local な custom property の型と初期値を定義できるようにすることが決まりました。 @mixin --card { @private { @property --space <length>: 1rem; } padding: var(--space); } Proposal: Custom Highlight API Extensions csswg-drafts#14613 Custom Highlight API で opacity と CSS Animations、CSS Transitions を利用できるようにすることが決まりました。多数の span 要素を生成せずに、Range で指定した文字列の背景色や文字色、text decoration を fade させられるようになります。 [css-grid-3][css-display-4] Naming for grid lanes reading-flow values csswg-drafts#14290 Grid Lanes の視覚的な配置に合わせて読み上げや focus navigation の順序を制御する、reading-flow の値が決まりました。 grid-lanes-track は track に沿った順序、grid-lanes-flow は item を配置する flow に沿った順序を表します。order property を反映する grid-lanes-order も追加されます。 .gallery { display: grid-lanes; reading-flow: grid-lanes-flow; } Blink: Intent to Prototype: CSS Linked Parameters for External SVGs groups.google.com 外部 SVG へ名前付きの CSS 値を渡し、SVG 側で env() を使って読み取れるようにする CSS Linked Parameters の Intent to Prototype です。 外部 SVG は参照元 document の style や custom property を継承しません。そのため、theme に応じて icon の色を変えるには、SVG を inline 化するか、色ごとに別の file を用意する必要がありました。この提案では、1 つの cache された SVG を再利用しながら、参照ごとに異なる値を渡せます。 img { link-parameters: param(--icon-color: green); } <circle fill="env(--icon-color, black)" /> WebKit Features for Safari 27.2 webkit.org Safari 27.2 では、Customizable Select と Grid Lanes の既存実装が追加されました。両実装とも Chrome に続く主要ブラウザ 2 つ目の実装です。 ARIA・WCAG feat: add listview role to ARIA aria#2951 TPAC にて、List View というインタラクティブなリスト構造のセマンティクスを定義する議論がありました。 listbox とは違ってフォーム要素ではなく、grid とは違って 1 次元であるような UI で、チャットのメッセージ一覧、最近開いたファイル一覧、ページネーション、カルーセルなどが例として挙げられています。 List View 内では Tab キーの代わりに矢印キーで操作できるようにすることで、ページを読み進めるのに Tab キーを押す手間を省くことができるというメリットが提示されています。 これに対して、新しく listview のような親 role と、それに対応する子 role を追加することが決定しました。そして、ul, ol 要素に listview role がつけられると、その子の li 要素には自動で子 role がつくようにするとのことです。 なお、role 名は未確定のようです。 参考 Interactive lists | Sarah Higley A proposal for a listview role to discuss during TPAC 2026 Filtering a listbox with a text field (filterable select) aria#2841 現在 Open UI で検討されている Filterable Select という UI のアクセシビリティマッピングについて、TPAC で議論がありました。 テキスト入力欄に隣接してリストボックスが存在している、GitHub のラベルフィルター UI のようなものが想定されています。 一見 combobox role が適切なように思えますが、今回のパターンは開閉状態を持たない、入力したテキストとは別で選択肢の値が送信値になる、入力しなくても直接選択肢を選択できるといった違いから、適さないという結論になりました。 また、aria-activedescendant を誤った使い方をしてアクセシビリティ上の問題が発生するケースも取り上げられ、aria-activedescendant の仕様が改善されることになりました。 [accname PR 235] feat: update step 2F to exclude nodes referenced by aria-actions aria#2208 accessible name に使われる text alternative の計算ステップにおいて、aria-actions で参照されているノードが除外されるようになるようです。 Simplify allowances for explicit declaration of implicit role on elements html-aria#612 button 要素の button is also allowed, but NOT RECOMMENDED. や del 要素の Any role, though deletion is NOT RECOMMENDED. など、要素が持つ暗黙の role を指定するのを非推奨であると記載するのをやめるという issue です。 暗黙の role を指定するのは多くの場合不要ですが、指定することでアクセシビリティを損なうこともありません。よって、基本的にはこのような記述を削除し、暗黙の role を指定することでアクセシビリティ上の問題が発生する場合のみ明記する方針になるとのことです。 browser accessibility heuristics wiki aria#2931 以前紹介した browser accessibility heuristics wiki のページに、WebKit で ul / ol に対して list-style-type を none に指定すると、list role がつかなくなるというヒューリスティックが記載されるようです。これは不具合ではなく意図的な動作です。 参考 x.com 170179 – VoiceOver does not announce a list for groups of links when list-style: none; ACD - Accessibility Compat Data acd.lolaslab.co ACD Project の Web サイトが公開されました。 ACD の概要や貢献方法、その他記事が公開されています。 JavaScript ECMA402 今月公開された、10 月 8 日の TG2 meeting notes の内容です。 github.com/tc39/ecma402/blob/main/meetings/notes-… How should JS handle un-deprecation of EST5EDT, CST6CDT, MST7MDT, PST8PDT in TZDB 2026d? ecma402#1103 EST5EDT などのタイムゾーン ID は、IANA の TZDB で一度非推奨になっていました。しかし互換性への苦情を受けて TZDB 2026d で非推奨が取り消され、再び独立したゾーン(primary zone)として扱われるようになりました。このままだと、これらの ID が Intl.supportedValuesOf("timeZone") の結果に含まれ、タイムゾーンの選択 UI にも表示されることになります。 これらのタイムゾーンは、過去や将来の日付で都市ベースのタイムゾーンと異なる時刻になることがあり、事情を知らないユーザーが選択 UI でこれらを選んでしまうことが懸念されていました。またこれらのタイムゾーンに対する現在のブラウザの挙動は 2 通りに分かれてしまっているという問題もあります。Firefox と Chrome はこれらを都市ベースの ID(America/New_York など)の alias として扱い、Safari は受け付けません。 会議の結果、これらを canonical ID(Intl.supportedValuesOf で列挙される正式な ID)として認めないことを仕様に明記し、alias として受け付けるかどうかは実装に任せることになりました。こうすると Firefox / Chrome / Safari の現在の挙動はどれも仕様に沿ったものになります。この方針で仕様の PR を作り、TG1(TC39 の本会議)に提出して承認を求める予定です。 add String.toTitleCase, String.toLocaleTitleCase ecma402#294 文字列をタイトルケース(単語の先頭を大文字にする形)に変換するメソッド toTitleCase / toLocaleTitleCase を追加しようという Issue です。CSS には text-transform: capitalize がありますが、JavaScript には同等の機能がありません。またアイルランド語のように通常より複雑な変換が必要な言語があり、CLDR や ICU がそれに対応していないという課題もあります。 会議の結果、CSSWG と今後の進め方を相談するため、TPAC のブレイクアウトセッションに申し込むことになりました。 Duration / time units in Amount proposal-amount#139 Amount は、数値と単位を組にして扱う ECMA-262 の提案です。この Issue では、minute-and-second のような時間の単位を持つ Amount をどうフォーマットするかが議論されました。Amount のフォーマットには Intl.NumberFormat を使いますが、複数の単位の組み合わせを表示する Intl Sequence Units が時間の単位を対象にしていないため、そのままではフォーマットできません。 Intl.DurationFormat でフォーマットすればよいという意見が出ましたが、その場合は toLocaleString() の options が NumberFormat 用と DurationFormat 用の 2 通りの形になってしまうという問題があります。そのため、時間の単位ではエラーを投げてもよいという意見もありました。 会議では結論が出ず、Amount を Temporal.Duration に変換する手段(Amount.prototype.toDuration を追加する、Temporal.Duration.from が Amount を受け取れるようにする、など)を別の Issue で検討することになりました。 Exact input handling for sequence unit values: Iterable vs. Array-like vs. IsArray proposal-intl-sequence-units#19 Intl Sequence Units で、フォーマットする値をどの形式(Iterable / Array-like / 配列のみ)で受け取るかについての Issue です。しかし、この Issue 自体が TC39 の AI ポリシーに違反しているという指摘があり、会議では内容を議論せずに延期することになりました。 Should we coerce the value field? proposal-intl-unit-protocol#6 Intl Unit Protocol は、value と unit のフィールドを持つオブジェクトを Intl.NumberFormat.prototype.format() に渡して、数値と単位をまとめてフォーマットできるようにする Stage 2 の提案です。この Issue では、value フィールドにも既存の format() と同じ型変換を行うか、つまり Symbol.toPrimitive を実装した独自の数値型も受け付けるかが議論されました。 会議の結果、Intl.NumberFormat が以前から独自の数値型を受け付けていることにそろえ、型変換を行う現在の仕様を維持する方向になりました。 Should we check for the protocol even if the formatter is not a unit/currency formatter? proposal-intl-unit-protocol#7 現在の仕様では、format() は style に関係なく、引数から常に Intl Unit Protocol のフィールドを読み取ります。そのため、単位を使わない NumberFormat(style: "decimal" など)にプロトコルのオブジェクトを渡したときにどう扱うかが問題になっていました。 会議の結果、style が "unit" か "currency" のときだけフィールドを読み取るように変更することになりました。仕様の挙動が変わる normative な変更なので、次回の TG1 に提出して承認を求める予定です。 Input unit type checking proposal-intl-unit-protocol#8 unit フィールドに不正な単位が入っていたときに、どの時点で例外を投げるかについての Issue です。フィールドの読み取りなど、外から観測できる処理との順序がまだ決まっていません。 会議の結果、この点は Stage 2.7 に向けたレビューで確認することになりました。 Cyclic year doesn't show up in resolvedOptions ecma402#816 中国暦のように干支で年を表すカレンダーで、Intl.DateTimeFormat.prototype.resolvedOptions() の結果に year が含まれないという Issue です。以前の会議で修正方針に合意していましたが、仕様と実装のどちらを直すべきかが改めて確認されました。 仕様では、表示パターンに relatedYear が含まれていれば resolvedOptions() の結果にも year が含まれると定めています。会議の結果、仕様は正しく実装が準拠していないことが確認され、この挙動を検証する test262 のテストを追加することになりました。 Normative: Define %TypedArray%.prototype.toLocaleString ecma402#1095 TG2 の議題ではありませんが、今月マージされた normative な PR です。ECMA-262 は、ECMA-402 も実装する処理系に対して、%TypedArray%.prototype.toLocaleString(Uint8Array などの型付き配列の toLocaleString)を Array.prototype.toLocaleString と同じように ECMA-402 の定義で置き換えるよう求めています。しかし、ECMA-402 にはその定義がありませんでした。 この PR で ECMA-402 に定義が追加され、各要素の toLocaleString に locales と options が渡されることが明記されました。 ECMA262 Deferred re-exports advanced to stage 2.7 tc39/proposal-deferred-reexports 再エクスポートに defer を付けることで、使われていないモジュールの読み込みと実行をスキップできるようにする提案です。 TC39 第 116 回の MTG で Stage 2 から Stage 2.7 へ昇格しました。 ただし、エディタ(ECMA262の仕様書の編集を行うグループ)のレビューが未完了のため、エディタ承認を条件とした昇格です。 ライブラリのエントリポイントがすべての公開 API を再エクスポートする「バレルモジュール」パターンでは、使わない機能まで含めてモジュールグラフ全体が読み込まれます。 バンドラの tree-shaking は副作用の扱いがツールごとに異なり、ブラウザで ESM を直接実行する場合は使えません。 この提案は、「使われなければ無視して良い」再エクスポートを表現できるようにするものです。 // math.js export defer { add } from "./math/add.js"; export defer { sub } from "./math/sub.js"; export { mul } from "./math/mul.js"; // main.js import { add, mul } as math from "./math.js"; import("./math.js", { exports: ["add", "mul"] }); // 動的 import 版 Composites advanced to Stage 2 tc39/proposal-composites Map や Set のキーとして使える、値ベースの等価性を持つ Composite を導入する提案です。 TC39 第 116 回の MTG で Stage 1 から Stage 2 へ昇格しました。 現在の Map / Set は要素の同一性判定に SameValueZero を使うため、オブジェクトは「自分自身とだけ等しい」扱いになります。 構造化された値をキーにしたくても、中身が同じ 2 つのオブジェクトは別物と判定されてしまい、この挙動を変える手段はありません。 回避策として JSON.stringify で文字列化してキーにする方法がありますが、キーの列挙順で結果が変わる、循環参照で例外になるなど、正しく使うのが難しいものでした。 Composite は「中身で等価判定したい場合に明示的に選べる別の手段」で、これを使うと同じ中身なら同じ参照になるオブジェクトを作れるようになります。 const pos1 = Composite({ x: 1, y: 4 }); const pos2 = Composite({ x: 1, y: 4 }); pos1 === pos2; // true(同じ値の組からは同じオブジェクトが返る) // 既存の Map / Set でそのままキーとして機能する const itemAtPosition = new Map(); itemAtPosition.set(pos1, "book"); itemAtPosition.get(Composite({ x: 1, y: 4 })); // "book" // キーはソートされ正規形を持つため、記述順は等価性に影響しない Composite({ b: 2, a: 1 }) === Composite({ a: 1, b: 2 }); // true Bigint from exponential advanced to stage 2 tc39/proposal-bigint-from-exponential 1e6n のような指数表記で BigInt を書けるようにし、BigInt("1e6") のような文字列からの変換も受け付けるようにする提案です。 TC39 第 116 回の MTG で Stage 1 から Stage 2 へ昇格しました。 // これまで: 桁を手で書く const scale = 1_000_000_000_000_000_000n; BigInt(1e21); // Number を経由するので精度が保証されない // この提案: 指数表記で書ける const scale2 = 1e21n; BigInt("1e21"); // 1000000000000000000000n BigInt("86.4e12"); // 86400000000000n(整数になる小数部は許容) Baseline 📃 October 2026 release notes web-platform-dx.github.io Misc THIS IS FOR EVERYONE の日本語訳が発売 str.toyokeizai.net ティム・バーナーズ=リー氏の THIS IS FOR EVERYONE の日本語訳本が、10/17 に発売開始されました。 Web Sustainability | Our mission | W3C w3.org W3C のミッションの 1 つとして、サステナビリティのページが追加されました。 W3C のサステナビリティへの取り組みの概要や、Sustainable Web Interest Group で作業中の Web Sustainability Guidelines の紹介があります。 Web Sustainability Guidelines については、以下の記事で紹介されています。 Web Sustainability Guidelinesとは Blink: Intent to Prototype: Decisions API groups.google.com 自然言語の入力を、構造化された質問と選択肢に照らして端末上で評価する Decisions API の Intent to Prototype です。Intents にも書かれている通り、ブラウザ上の System One モデル (e.g. Jev) のようなものです。 現時点では design sketch の段階で、仕様や ship の計画はありません。prototype を通して、FB を募り、今後の方針を決めていく段階でしょう。 Decisions API Explainer

  2. @tobi I think that's the TC39 Type Annotations proposal tc39/proposal-type-annotations

    8 likes · 952 views
  3. What JavaScript using disposes after returns and exceptions: nine checks using registers an object with a disposal method in a scope and calls cleanup when that scope ends. When several resources were registered, they were disposed in reverse acquisition order. An early return followed the same rule. Nine conditions in Node.js covered disposal order, retained exceptions, and asynchronous waiting. Before using it, check which object supplies disposal and which scope exit triggers cleanup. Disposal finishes before the caller receives a return value This code ran as an .mjs file in Node.js 24.15.0, without a transpiler or polyfill: const events = []; function open(name) { events.push(`open:${name}`); return { [Symbol.dispose]() { events.push(`close:${name}`); } }; } function work() { using first = open('A'); using second = open('B'); events.push('body'); return 'done'; } const value = work(); console.log(JSON.stringify({ value, events })); The result contained value: "done", with events open:A → open:B → body → close:B → close:A. Both disposals had finished when the caller received the return value. open only returns a fixture object. The log records calls to Symbol.dispose; it is not a measurement of real files or connections. The ECMAScript declaration and scope specification also describes disposing resources registered through using when scope processing finishes. The comparison included failures as well as normal completion: | Condition | Observed order or result | | --- | --- | | Acquire A and B, finish normally | Dispose B, then A | | Acquire A and return | Dispose A before returning to the caller | | Throw in the body | Dispose A before entering catch | | Acquire A, then throw while acquiring B | Dispose registered A; no disposal call for B | | Body and both A/B disposals throw | Attempt both disposals and retain errors through SuppressedError | | Register A and B with await using | Finish B's asynchronous disposal before disposing A | | Pass an asyncDispose-only value to using | TypeError; asynchronous disposal is not called | | Pass null and undefined to using | Execute the body without disposal methods | | Throw while using a temporary directory | Directory no longer exists after entering catch | When acquiring B throws, using does not dispose B afterward. If the acquisition function allocated something before failing, that function needs to clean up its partial acquisition. Where does the body error go if cleanup fails too? A deliberately failing disposer tested this case. The body threw body failed, B threw close failed:B, and A threw close failed:A. The caught exception's error.message was close failed:A. The body failure was still retained: | Reference | Recorded message | | --- | --- | | error.error.message | close failed:A | | error.suppressed.error.message | close failed:B | | error.suppressed.suppressed.message | body failed | Both the outer and inner wrappers were SuppressedError objects. Cleanup continued after a disposal failed, with earlier errors retained in a related chain. The explicit resource management design material explains this relationship. Logging only the outer message can miss the body failure. The experiment followed both error and suppressed and checked each message. Use await using for asynchronous disposal The asynchronous fixture recorded the start and end of Symbol.asyncDispose separately. Registering await using a = ... and await using b = ... produced body → close start:B → close end:B → close start:A → close end:A → after work. The wait in this fixture was Promise.resolve(). It did not measure network or disk I/O duration. It did verify that A's disposal started after B's completed, and that the caller continued after both finished. Passing an object with only Symbol.asyncDispose to using without await produced a TypeError. An asynchronously disposable value cannot simply be placed in an ordinary using declaration. Check a real Node.js temporary directory The next example uses an actual temporary directory rather than only a logging object: import { existsSync, mkdtempDisposableSync, writeFileSync } from 'node:fs'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; let path; try { using temporary = mkdtempDisposableSync(join(tmpdir(), 'article-using-example-')); path = temporary.path; writeFileSync(join(path, 'sample.txt'), 'fixture'); console.log('inside:', existsSync(path)); throw new Error('work failed'); } catch (error) { console.log('caught:', error.message); } console.log('after:', existsSync(path)); The output was inside: true, caught: work failed, and after: false. The directory, including sample.txt, was removed when the exception left the scope. Node.js 24.15.0's mkdtempDisposableSync returns an object with a path and a disposal method for removal. This API was added in 24.4.0. An arbitrary path string does not acquire that deletion behavior merely by being used in a using declaration. The tests ran on September 11, 2026, with Node.js 24.15.0 on macOS arm64. The nine-condition fixture and results are available. Other Node versions, browsers, transformed code, and forced process termination were not tested. These results cover disposal when JavaScript can execute the scope-exit steps.

  4. Temporal API in TypeScript: deadlines and time zones Being able to use the Temporal API in TypeScript was one of my favorite pieces of news around Node.js 26. Over the years I have seen plenty of code where a deadline was off by an hour because somebody wrote “plus 48 hours” instead of “plus two days” and a daylight saving change happened in between. Bugs like that don’t show up in tests. They show up in production, twice a year. In my article on Node.js 26 LTS on Lambda I only touched on Temporal briefly. This time we go deeper: step by step, you build the deadline logic for a fictional city library, store the values cleanly in DynamoDB, deploy everything to Lambda with the Serverless Framework and deliberately test across the DST change. Afterwards you will know which Temporal type is meant for what and how to move away from Date or date-fns. > In short > > Temporal replaces Date with dedicated types for instants, calendar dates and times in a time zone. Node.js 26 ships the API without a flag, TypeScript has included the types since version 6.0, and older Lambda runtimes only need a small polyfill. The biggest pitfall is choosing the wrong type: if you store a deadline as a plain date without a time zone, you only move the problem somewhere else. What you will learn in this article - Which Temporal type is right for instants, calendar days and deadlines - How to set up Temporal in TypeScript, Node.js 26 and on Lambda with nodejs24.x - How to calculate and test deadlines correctly across DST changes - How to store Temporal values in DynamoDB, including TTL - How to migrate step by step from Date or date-fns Why Date so often gets deadlines wrong Date has been part of JavaScript since 1995 and was closely modeled on an early Java class at the time. You can still tell. Months start at 0, so new Date(2026, 9, 25) is October 25. A Date object is mutable, and if you pass it to a function you may get it back changed. On top of that, Date knows exactly two time zones: UTC and the local time zone of the machine running the code. That last point is especially tricky in a Serverless setup. A Lambda function runs in UTC by default. Your customers, however, live in Berlin, Vienna or Zurich. Ask Lambda for “today” at 0:30 am German time and you still get yesterday. Lambda is doing everything right here. The real question is: whose “today” do you mean? The classic example is the DST change. On October 25, 2026, clocks in Germany go back from 3:00 am to 2:00 am, so that day has 25 hours. If you create a deadline of “two days” on October 24 at 10:00 am and calculate it as 48 hours, you end up at 9:00 am on October 26. What you meant was 10:00 am. That is why Temporal clearly separates exact durations in hours from calendar arithmetic in days. const pickup = Temporal.ZonedDateTime.from("2026-10-24T10:00[Europe/Berlin]"); console.log(pickup.add({ hours: 48 }).toString()); // 2026-10-26T09:00:00+01:00[Europe/Berlin] (exactly 48 hours later) console.log(pickup.add({ days: 2 }).toString()); // 2026-10-26T10:00:00+01:00[Europe/Berlin] (two calendar days later) Both results are correct. They simply answer different questions. With Date you have to keep that difference in your head, while Temporal makes you write it down. Which Temporal type for which purpose? Temporal is a namespace like Math, not a constructor. It contains several classes, and all objects are immutable: every operation returns a new object. For everyday work you mainly need these types: | Type | What it describes | Typical example | | --- | --- | --- | | Temporal.Instant | Exact point on the timeline, without calendar or time zone | Event timestamp, log entry | | Temporal.ZonedDateTime | Instant plus time zone, handles summer and winter time | Return deadline “Nov 19 at 6:00 pm in Berlin” | | Temporal.PlainDate | Calendar date without time and without time zone | Birthday, day of borrowing, holiday | | Temporal.PlainTime | Wall-clock time without a date | Closing time “18:00” | | Temporal.PlainDateTime | Date and time without a time zone | Form input before the time zone is known | | Temporal.Duration | Length of time in years, months, days, hours … | Loan period, gap between two appointments | There are also PlainYearMonth (for a billing month, say), PlainMonthDay (recurring anniversaries) and Temporal.Now for the current time in various forms. !Decision tree: need an exact instant? Yes leads to Instant or ZonedDateTime, No to PlainDate or PlainTime Rule of thumb for choosing a Temporal type The rule of thumb for choosing a type For every value I ask myself one single question: do I need to know exactly when this happens in the world? If yes, I need an Instant or a ZonedDateTime. A return deadline belongs in this group, because at 6:01 pm in Berlin it has expired, no matter where the server is. If the answer is no, a plain type is enough. A birthday is on May 3, whatever time zone you happen to be in. A mistake I see often: deadlines get stored as PlainDateTime because it looks so simple. Then exactly the information you later need for comparisons is missing. PlainDateTime is meant for intermediate steps, for example a form before it is clear which time zone the appointment takes place in. Setting up the Temporal API in TypeScript Before you start, a quick look at support. Temporal has reached stage 4 in the TC39 process, the standards committee for JavaScript, which means it is fully specified. Firefox has shipped the API since version 139, Chrome since version 144 and Node.js since version 26. According to the proposal repository, Safari is still missing. TypeScript has included the matching types since version 6.0. Prerequisites - Node.js 26 locally (for native Temporal support) or Node.js 22/24 with a polyfill - TypeScript 6.0 or later; the example was tested with TypeScript 7.0.2 - For deployment: an AWS account and the Serverless Framework v4 (npm i -g serverless) - Basic knowledge of Lambda and DynamoDB helps but isn’t required Lambda runtime and polyfill On Lambda, nodejs26.x is still a public preview, and AWS plans the regular launch for November 2026. Preview runtimes come without the Lambda SLA and are not intended for production use. Until then, use nodejs24.x and load the temporal-polyfill package (MIT license). Its entry point temporal-polyfill/global installs Temporal globally only if the runtime doesn’t already provide it. I checked this in the package’s source code: on Node.js 26 the import simply does nothing. So later you can switch the runtime without touching a single line of code. Configuring TypeScript You enable the types via the lib option. Either take all of esnext or only the esnext.temporal part. I prefer the more granular option because it makes clear which new features the project actually uses. { "compilerOptions": { "target": "es2024", "lib": ["es2024", "esnext.temporal"], "module": "nodenext", "moduleResolution": "nodenext", "strict": true, "noEmit": true, "allowImportingTsExtensions": true, "erasableSyntaxOnly": true, "verbatimModuleSyntax": true, "types": ["node"], "skipLibCheck": true }, "include": ["src", "test"] } allowImportingTsExtensions and erasableSyntaxOnly let Node.js run the TypeScript files directly. We will use that for the tests in a moment, without any build step. Step by step: loan deadlines for a library Our example is a fictional city library with clear rules. A book may be borrowed for 21 days. If the last day falls on a Sunday or a holiday, the return moves to the next opening day. You can return books until 6:00 pm Berlin time. Two days before the deadline a reminder goes out at 9:00 am, and every overdue day costs 50 cents, up to a maximum of 10 euros. Step 1: Create the project mkdir temporal-library-loans && cd temporal-library-loans npm init -y npm pkg set type=module npm i temporal-polyfill @aws-sdk/client-dynamodb @aws-sdk/lib-dynamodb npm i -D typescript @types/node @types/aws-lambda mkdir src test Then create the tsconfig.json shown above. Next comes a tiny file that loads the polyfill when needed: // src/temporal.ts // Loads the polyfill only if the runtime has no native Temporal // (e.g. nodejs24.x on Lambda). On Node.js 26 this does nothing. import "temporal-polyfill/global"; Step 2: The deadline logic as pure functions The rules go into a file without any AWS dependencies. That makes them easy to test and independent of where the code runs later. // src/loan-rules.ts import "./temporal.ts"; export const LIBRARY_ZONE = "Europe/Berlin"; export const LOAN_DAYS = 21; export const CLOSING_TIME = Temporal.PlainTime.from("18:00"); export const FEE_PER_DAY_CENTS = 50; export const MAX_FEE_CENTS = 1000; // Holidays when the library is closed (fictional selection) export const HOLIDAYS = new Set(["2026-12-25", "2026-12-26", "2027-01-01"]); function isClosed(date: Temporal.PlainDate): boolean { return date.dayOfWeek === 7 || HOLIDAYS.has(date.toString()); } // Due date: day of borrowing + 21 days; on a Sunday or holiday, the next opening day export function dueDate(borrowedOn: Temporal.PlainDate): Temporal.PlainDate { let due = borrowedOn.add({ days: LOAN_DAYS }); while (isClosed(due)) { due = due.add({ days: 1 }); } return due; } // Exact deadline: due date at 6:00 pm Berlin time export function returnDeadline(due: Temporal.PlainDate): Temporal.ZonedDateTime { return due.toZonedDateTime({ timeZone: LIBRARY_ZONE, plainTime: CLOSING_TIME }); } // Reminder two calendar days earlier at 9:00 am – wall-clock time, even across DST changes export function reminderAt(deadline: Temporal.ZonedDateTime): Temporal.ZonedDateTime { return deadline.subtract({ days: 2 }).with({ hour: 9, minute: 0 }); } // Overdue days: calendar days between due date and today (never negative) export function overdueDays(due: Temporal.PlainDate, today: Temporal.PlainDate): number { const days = due.until(today, { largestUnit: "days" }).days; return Math.max(0, days); } export function lateFeeCents(due: Temporal.PlainDate, today: Temporal.PlainDate): number { return Math.min(overdueDays(due, today) * FEE_PER_DAY_CENTS, MAX_FEE_CENTS); } // "Today" from the library's point of view – not from the Lambda server's (UTC) export function libraryToday(now: Temporal.Instant): Temporal.PlainDate { return now.toZonedDateTimeISO(LIBRARY_ZONE).toPlainDate(); } A few spots deserve a second look. The due date is a PlainDate, because “21 days later” is purely a calendar question. dayOfWeek follows the ISO standard from 1 (Monday) to 7 (Sunday), so the Sunday-is-zero confusion of Date is finally gone. Only in returnDeadline do time and time zone come in, turning the calendar day into an actual instant. For the reminder, we deliberately calculate in calendar days and then set the time with with(). If the DST change falls between reminder and deadline, it still stays at 9:00 am wall-clock time. That is exactly what people expect when a reminder is supposed to arrive “at nine in the morning”. The libraryToday function looks harmless but matters a lot. It receives an Instant and converts it into the library’s time zone. As a result, “today” is always Berlin’s today, even though Lambda runs in UTC. And because the instant comes from outside, you can simulate any time you like in tests. Step 3: Storing Temporal values in DynamoDB DynamoDB has no date type. So you have to decide in which form to store the values, and that decision matters more than it seems. My suggestion: - Plain calendar dates as ISO strings like 2026-11-19. They are readable and sort lexicographically. - Instants as UTC strings like 2026-11-19T17:00:00Z. That way comparisons and range queries on a sort key work. - The time zone separately as an IANA name like Europe/Berlin, so you can restore local time for display. - For DynamoDB TTL, an additional number with epoch seconds, because TTL ignores anything that isn’t a Number attribute. // src/storage.ts import "./temporal.ts"; export interface Loan { loanId: string; bookId: string; memberId: string; borrowedOn: Temporal.PlainDate; due: Temporal.PlainDate; deadline: Temporal.ZonedDateTime; } export interface LoanItem { pk: string; bookId: string; memberId: string; borrowedOn: string; // "2026-10-29" – plain date, no time dueDate: string; // "2026-11-19" deadlineAt: string; // "2026-11-19T17:00:00Z" – UTC, sortable timeZone: string; // "Europe/Berlin" – for display expiresAt: number; // epoch seconds for DynamoDB TTL } const KEEP_AFTER_DEADLINE = { days: 180 }; export function toItem(loan: Loan): LoanItem { return { pk: `LOAN#${loan.loanId}`, bookId: loan.bookId, memberId: loan.memberId, borrowedOn: loan.borrowedOn.toString(), dueDate: loan.due.toString(), deadlineAt: loan.deadline.toInstant().toString(), timeZone: loan.deadline.timeZoneId, expiresAt: Math.floor(loan.deadline.add(KEEP_AFTER_DEADLINE).epochMilliseconds / 1000), }; } export function fromItem(item: LoanItem): Loan { return { loanId: item.pk.replace("LOAN#", ""), bookId: item.bookId, memberId: item.memberId, borrowedOn: Temporal.PlainDate.from(item.borrowedOn), due: Temporal.PlainDate.from(item.dueDate), deadline: Temporal.Instant.from(item.deadlineAt).toZonedDateTimeISO(item.timeZone), }; } Why not just store the string of a ZonedDateTime, i.e. 2026-11-19T18:00:00+01:00[Europe/Berlin]? It does contain everything, but it doesn’t sort reliably once values with different offsets end up in the same column. The UTC instant doesn’t have that problem. You can read more about TTL and expiry dates in my post DynamoDB TTL: build seat holds that expire. Step 4: Lambda handler and serverless.yml The handler is intentionally lean. It reads the book and member from the request, calculates date, deadline and reminder and writes the record to DynamoDB. // src/handler.ts import { randomUUID } from "node:crypto"; import { DynamoDBClient } from "@aws-sdk/client-dynamodb"; import { DynamoDBDocumentClient, PutCommand } from "@aws-sdk/lib-dynamodb"; import type { APIGatewayProxyEventV2, APIGatewayProxyResultV2 } from "aws-lambda"; import { dueDate, libraryToday, reminderAt, returnDeadline } from "./loan-rules.ts"; import { toItem, type Loan } from "./storage.ts"; const db = DynamoDBDocumentClient.from(new DynamoDBClient({})); const TABLE = process.env.LOANS_TABLE ?? ""; export async function createLoan(event: APIGatewayProxyEventV2): Promise<APIGatewayProxyResultV2> { const body = JSON.parse(event.body ?? "{}") as { bookId?: string; memberId?: string }; if (!body.bookId || !body.memberId) { return { statusCode: 400, body: JSON.stringify({ message: "bookId and memberId are required" }) }; } const borrowedOn = libraryToday(Temporal.Now.instant()); const due = dueDate(borrowedOn); const deadline = returnDeadline(due); const loan: Loan = { loanId: randomUUID(), bookId: body.bookId, memberId: body.memberId, borrowedOn, due, deadline, }; await db.send(new PutCommand({ TableName: TABLE, Item: toItem(loan) })); return { statusCode: 201, body: JSON.stringify({ loanId: loan.loanId, dueDate: due.toString(), deadline: deadline.toString(), reminder: reminderAt(deadline).toString(), }), }; } > 🎬 Flow of a book loan with Temporal – this animated diagram only runs on my site: watch it on sascha-ms.de For the infrastructure I am using the Serverless Framework here. Since version 4 it builds TypeScript itself with esbuild, so no extra build configuration is needed. # serverless.yml service: library-loans frameworkVersion: "4" provider: name: aws runtime: nodejs24.x # switch to nodejs26.x once it is GA region: eu-central-1 architecture: arm64 memorySize: 256 environment: LOANS_TABLE: ${self:service}-${sls:stage}-loans TZ: UTC # Lambda default; library time comes from the code iam: role: statements: - Effect: Allow Action: - dynamodb:PutItem Resource: - !GetAtt LoansTable.Arn functions: createLoan: handler: src/handler.createLoan events: - httpApi: path: /loans method: post resources: Resources: LoansTable: Type: AWS::DynamoDB::Table Properties: TableName: ${self:service}-${sls:stage}-loans BillingMode: PAY_PER_REQUEST AttributeDefinitions: - AttributeName: pk AttributeType: S KeySchema: - AttributeName: pk KeyType: HASH TimeToLiveSpecification: AttributeName: expiresAt Enabled: true The IAM role may only call PutItem on exactly this table. The handler needs nothing more. Deploy and try it out: serverless deploy curl -X POST "https://<your-api-id>.execute-api.eu-central-1.amazonaws.com/loans" \ -H "Content-Type: application/json" \ -d '{"bookId":"book-42","memberId":"reader-7"}' If you borrow on October 29, 2026, the response looks roughly like this (the loanId will of course differ): { "loanId": "3f2c9a4e-8b1d-4c57-9e0a-5d6f7a8b9c01", "dueDate": "2026-11-19", "deadline": "2026-11-19T18:00:00+01:00[Europe/Berlin]", "reminder": "2026-11-17T09:00:00+01:00[Europe/Berlin]" } Step 5: Testing across the DST change Now comes the part I consider most important for date logic. Tests with “today plus three days” are almost always green and prove little. They only become meaningful with fixed dates around the DST change, holidays and the day boundary between UTC and Berlin. // test/loan-rules.test.ts import { test } from "node:test"; import assert from "node:assert/strict"; import { dueDate, lateFeeCents, libraryToday, overdueDays, reminderAt, returnDeadline } from "../src/loan-rules.ts"; const day = (s: string) => Temporal.PlainDate.from(s); test("due date is 21 days after borrowing", () => { assert.equal(dueDate(day("2026-10-29")).toString(), "2026-11-19"); }); test("holidays and Sundays move to the next opening day", () => { // Dec 25 holiday, Dec 26 holiday, Dec 27 Sunday assert.equal(dueDate(day("2026-12-04")).toString(), "2026-12-28"); }); test("deadline stays at 6:00 pm Berlin time, even after the DST change", () => { const deadline = returnDeadline(day("2026-10-26")); assert.equal(deadline.toString(), "2026-10-26T18:00:00+01:00[Europe/Berlin]"); assert.equal(deadline.toInstant().toString(), "2026-10-26T17:00:00Z"); }); test("reminder at 9:00 am wall-clock time across the DST change", () => { const reminder = reminderAt(returnDeadline(day("2026-10-26"))); assert.equal(reminder.toString(), "2026-10-24T09:00:00+02:00[Europe/Berlin]"); }); test("overdue days and capped fee", () => { assert.equal(overdueDays(day("2026-11-19"), day("2026-11-23")), 4); assert.equal(overdueDays(day("2026-11-19"), day("2026-11-10")), 0); assert.equal(lateFeeCents(day("2026-11-19"), day("2026-11-23")), 200); assert.equal(lateFeeCents(day("2026-11-19"), day("2026-12-31")), 1000); }); test("“today” is based on Berlin, not UTC", () => { // 23:30 UTC is already the next day in Berlin assert.equal(libraryToday(Temporal.Instant.from("2026-10-29T23:30:00Z")).toString(), "2026-10-30"); }); You can run the tests directly with Node.js, without a compile step: npx tsc --noEmit node --test test/*.test.ts Expected result: all tests pass. I ran them on Node.js 26.13 with native Temporal and on Node.js 22 with the polyfill, both times with identical results. The repository also contains a second test for the round trip through DynamoDB and the conversion from Date, which I leave out here to save space. Summer and winter time: what Temporal decides behind the scenes Twice a year there are times in Berlin that are ambiguous. In spring an hour is missing: on March 28, 2027 the clock jumps from 2:00 am to 3:00 am, so “2:30 am” doesn’t exist that day. In autumn an hour happens twice: on October 25, 2026, 2:30 am occurs once in summer time and once in winter time. Temporal solves this with the disambiguation option. The default value compatible behaves like Date: inside the gap the time is pushed forward, and in the repeated hour the earlier one wins. With earlier or later you pick a variant on purpose, and with reject Temporal throws a RangeError. // Spring gap: 2:30 am does not exist Temporal.ZonedDateTime.from("2027-03-28T02:30[Europe/Berlin]").toString(); // 2027-03-28T03:30:00+02:00[Europe/Berlin] // Repeated hour in autumn: deliberately pick the second 2:30 am Temporal.PlainDateTime.from("2026-10-25T02:30") .toZonedDateTime("Europe/Berlin", { disambiguation: "later" }) .toString(); // 2026-10-25T02:30:00+01:00[Europe/Berlin] // Often the best choice for booking systems: an error beats a silent shift Temporal.PlainDateTime.from("2027-03-28T02:30") .toZonedDateTime("Europe/Berlin", { disambiguation: "reject" }); // RangeError Something similar applies to month arithmetic. January 31 plus one month gives February 28 with the default overflow: "constrain". If you prefer an error, pass { overflow: "reject" }. For subscriptions and contract terms this is a business decision that you should clarify with the domain experts instead of leaving it to chance. Migrating from Date and date-fns Hardly any project can switch everything overnight. It doesn’t have to. I would use Temporal first in new business logic and convert at the edges, wherever older libraries, ORMs or SDKs still expect or return Date. // src/legacy.ts import "./temporal.ts"; // Existing code still returns Date objects? This converts them without loss. export function fromLegacyDate(value: Date): Temporal.Instant { return Temporal.Instant.fromEpochMilliseconds(value.getTime()); } // For libraries that still expect Date (e.g. older SDKs) export function toLegacyDate(value: Temporal.Instant): Date { return new Date(value.epochMilliseconds); } Going through epoch milliseconds is lossless, because Date only knows milliseconds anyway. Native runtimes and the polyfill also offer Date.prototype.toTemporalInstant(). I still prefer the explicit function, because it shows in the code where the old world ends. With date-fns, Day.js or Luxon it pays to look at what the library is actually used for. Often it is a handful of functions like addDays, differenceInDays or format. Temporal replaces the first two directly with add() and until(). For formatting there is toLocaleString(), which uses Intl.DateTimeFormat internally. Temporal.PlainDate.from("2026-11-19").toLocaleString("en-US", { dateStyle: "full" }), for example, returns “Thursday, November 19, 2026”. Running both side by side is fine for a transition period. The only important thing is that a function works either with Temporal or with Date, not with both. Common mistakes with the Temporal API in TypeScript A few pitfalls kept coming up while I was trying things out. - Comparing with === or <: Two Temporal objects with the same value are still different objects. Use equals() or the static compare() method. Comparing with < deliberately throws a TypeError. - Temporal.Now deep inside business logic: Then you can’t control time in tests. Get the current time at the edge, for example in the handler, and pass it on as a parameter. - JSON there, but not back: JSON.stringify turns Temporal objects into strings. When reading them back, you have to convert them with from() yourself, or you keep calculating with strings. - structuredClone and workers: Temporal objects can’t be structured-cloned, and Node.js throws a DataCloneError. Pass strings and rebuild the objects on the other side. - Storing an offset instead of a time zone: +01:00 is not a time zone. In summer, Berlin uses +02:00. If you only know the offset, you can’t calculate correctly into the future. Cost, performance and security The polyfill has a price, but it is modest. In my example, the minified handler including the polyfill weighs about 60 KB, not counting the AWS SDK, which is part of the runtime. For a Lambda cold start that hardly matters. Once nodejs26.x is generally available, you can remove the polyfill and save those bytes entirely. DynamoDB TTL costs nothing extra. Expired items are deleted without consuming write capacity, though not immediately: according to AWS, typically within a few days. Queries should therefore filter out expired items themselves. With global tables, however, the replicated deletes in other regions do consume write capacity. Two notes on security. Always validate date values from requests with from() inside a try block, because invalid input throws a RangeError. And don’t accept time zones from requests unchecked; compare them against a list of allowed zones. For scheduled reminders, EventBridge Scheduler is a good fit, since it understands cron expressions with a time zone. My EventBridge cron explainer shows how such expressions are built. My take I consider Temporal one of the most useful additions JavaScript has received in recent years. You could build correct code with Date, too. What makes Temporal valuable is that it forces the right questions. If you have to choose between PlainDate and ZonedDateTime, you automatically think about what a value means in business terms. At the same time, I wouldn’t rebuild everything at once. Existing code with date-fns that has been running reliably for years is not an emergency. New business logic, though, I would write with Temporal starting now, on Lambda with the polyfill for the time being. For frontends that also serve Safari, the polyfill remains mandatory for a while anyway. One point where I stay skeptical: the API is large. Teams should agree on a few types and clear storage formats, otherwise you quickly end up with a new kind of sprawl, just with more modern names. FAQ Can I use the Temporal API in production already? Yes. Temporal is fully standardized and available without a flag in Node.js 26, Chrome and Firefox. For Safari and older Node versions such as nodejs24.x on Lambda, use a polyfill like temporal-polyfill. Which TypeScript version do I need for Temporal? The types are built in from TypeScript 6.0. You enable them with "lib": ["esnext"], with "target": "esnext" or specifically with esnext.temporal. What is the difference between Instant and ZonedDateTime? Both describe an exact point in time. An Instant, however, has no time zone and therefore no calendar. ZonedDateTime also knows the time zone in which the instant is viewed, so it can calculate correctly with days, months and DST changes. Does Temporal replace date-fns, Luxon or Day.js? For arithmetic, comparisons and time zones, in many cases yes. For formatting, Temporal uses Intl.DateTimeFormat. Special features like relative times (“3 days ago”) are covered by Intl.RelativeTimeFormat or still by a library. Your next step Find a place in your project where a deadline or appointment is calculated with Date today, and write a test for October 25 or March 28. If it passes, great. If not, you have found your first candidate for Temporal. How do you handle time zones in your Lambda functions so far: everything in UTC with conversion in the frontend, or do you already calculate with local time in the backend? Let me know in the comments. Time zones matter not only for deadlines but also for triggering jobs. I show how to start cron jobs reliably at German local time in EventBridge Scheduler time zone: cron at local time. Sources - TC39: Temporal proposal (stage 4, implementation status) - MDN: Temporal - Node.js: release notes v26.0.0 - TypeScript blog: Announcing TypeScript 6.0 - AWS: Lambda runtimes (nodejs26.x as public preview) - temporal-polyfill on GitHub - AWS: Time to Live in DynamoDB This post was created with AI assistance. --- 👉 Next step: Curious what your own Lambda functions cost? Try my Lambda cost calculator – it runs right in your browser. Originally published on sascha-ms.de. More articles on serverless, AI in everyday development and leading tech teams: sascha-ms.de

    1 reaction
  5. Stop Spreading Iterables Into Arrays to Join Them. Iterator.concat() Is Here TL;DR: Iterator.concat(a, b, c) walks any iterables one after another and returns a lazy iterator. No temporary array, no hand-written generator, and it has been Baseline Newly available since March 2026. The array you build just to throw away You have an array, a Set, and a generator. You want to loop over all of them in order. So you write the line everyone writes: for (const item of [...pinned, ...recentSet, ...loadMore()]) { render(item); } It works. It also copies every item into a brand new array, runs the generator to the very end, and then hands the array to the garbage collector a moment later. If you only needed the first few items, you paid for all of them. The careful version is a generator with yield*: function* concat(...iterables) { for (const iterable of iterables) { yield* iterable; } } Correct, lazy, and copy-pasted into a utils folder near you. What it is Iterator.concat() is that generator, built into the language. It takes any number of iterables and returns an iterator that yields from each one in turn: const array = [1, 2, 3]; const set = new Set([4, 5]); function* gen() { yield 6; yield 7; } console.log([...Iterator.concat(array, set, gen())]); // [1, 2, 3, 4, 5, 6, 7] The result is a real iterator, so the iterator helpers work on it directly: const evensTimesTen = Iterator.concat([1, 2, 3], new Set([4, 5, 6])) .filter((n) => n % 2 === 0) .map((n) => n * 10) .toArray(); console.log(evensTimesTen); // [20, 40, 60] Maps are iterables of [key, value] pairs, which makes "defaults, then overrides" a one-liner: const defaults = new Map([["theme", "light"], ["lang", "en"]]); const user = new Map([["theme", "dark"]]); const settings = new Map(Iterator.concat(defaults, user)); console.log(Object.fromEntries(settings)); // { theme: "dark", lang: "en" } Later keys win, because that is how the Map constructor already behaves. The realistic one: take only what you need Here is where laziness pays. Two sources of 1000 rows each, and a UI that shows the first three: let pulled = 0; function* page(start) { for (let i = start; i < start + 1000; i++) { pulled++; yield i; } } const spread = [...page(0), ...page(1000)].slice(0, 3); console.log(spread, pulled); // [0, 1, 2] 2000 pulled = 0; const lazy = Iterator.concat(page(0), page(1000)).take(3).toArray(); console.log(lazy, pulled); // [0, 1, 2] 3 Same answer. The spread version produced 2000 rows to keep 3. The concat version produced 3. Replace "row" with "parsed log line" or "decoded image" and that gap stops being academic. It is also lazy about opening each source. Logging every [Symbol.iterator]() call on two iterables gave this order: created open a next 1 next 2 open b next 3 Nothing is opened when you call concat, and b is not touched until a runs out. That also means infinite sources are fine, as long as you stop early: function* naturals() { let i = 1; while (true) yield i++; } console.log(Iterator.concat(["start"], naturals()).take(5).toArray()); // ["start", 1, 2, 3, 4] Can I use it? | Where | Status | |---|---| | Baseline | Newly available since March 2026 | | Chrome and Edge | 146 | | Firefox | 147 | | Safari (macOS and iOS) | 26.4 | | Node.js 24 | Not yet: typeof Iterator.concat is "undefined" in v24.11.0 | | Spec | TC39 Iterator Sequencing proposal, Stage 4 | Everything in this post ran in Chrome 156. If you need it in Node today, keep the five-line generator from above as a fallback. Gotchas 1. Strings are rejected, even though they are iterable. Iterator.concat([1], "ab") throws TypeError: Iterator.concat called on non-object right away, at the call, not later during iteration. null throws the same error. Wrap a string first: Iterator.from("bc") or [..."bc"] both worked. 2. Bare iterators need Iterator.from(). An object with only a next() method is not iterable, and passing one threw TypeError: #<Object> is not iterable. Built-in iterators like [1, 2, 3].values() are fine, because they are iterable too. 3. The result is single use. Spreading the same concat result twice gave [1, 2, 3] the first time and [] the second. It is an iterator, not an array, so call Iterator.concat again if you need a second pass. 4. Breaking early closes what it opened. With two generators that log in finally, a for...of that broke on the first item of the second one logged closed a when a ran out and closed b on the break. Your cleanup code runs, which is exactly what you want. Wrap-up Iterator.concat() is the yield* helper you kept rewriting, now with iterator helpers attached. Next time you type [...a, ...b] only to loop over it once, try Iterator.concat(a, b) instead. Sources - Iterator.concat() (MDN) - Iterator Sequencing proposal (TC39) - Finished proposals (TC39) - Iterator browser compatibility data (MDN BCD) This article was written with the help of AI and checked against the sources above.

    2 comments
  6. It would be interesting if browsers supported more languages but I don't mind the monoculture. I have spent a lot of time investing in languages, frameworks and tools, so much of that learning is both useless and worthless today. Most of what I have mastered isn't viable to use for anything in 2026. About ten years ago I deliberately started doubling-down on JavaScript instead, it's good-enough for practically any use case and I've been able to leverage it for web, mobile, desktop, server and games. There are things other languages can do better, but I love not having to install them and manage their different versions and dependencies, and stay familiar with their idiomatic approaches, and keep up-to-date with their evolution. There are outcomes where a monoculture can be bad, but structurally I think JavaScript is in a good place - open standard, open governance, open to proposals, massive open source culture. github.com/tc39/ecma262/blob/main/CONTRIBUTING.md

  7. Discord の ID を JSON.parse したら末尾が 063 から 060 になった。Snowflake は BigInt で読む 背景 自分のサイトに、Discord や X(旧Twitter)の ID から作成日時を取り出すツールを置いています。 作り始めたとき、テスト用の JSON を JSON.parse して ID を取り出したら、末尾の数字が変わっていました。 知識としては知っていたのですが、自分で踏むまでは他人事でした… Snowflake は、Twitter が考えた ID の作り方だ。 64 ビットの整数の上のほうにミリ秒単位の時刻、下のほうにどのサーバーで何番目に作ったかを詰める。 Discord も同じ形で、時刻の起点(エポック)だけが違う。 結論 Snowflake ID は Number で持てません。 - JSON.parse で数値のまま読むと、下位の桁が丸められて別の ID になる - 文字列のまま受け取り、ビット演算は BigInt で行う - Node.js v24 なら、JSON.parse の reviver で元の数字の文字列も取れる JavaScript の Number は小数を含む64ビットの浮動小数点数で、整数として正確に扱えるのは Number.MAX_SAFE_INTEGER(9007199254740991、約 9×10^15)までである。 Snowflake は 18〜19 桁あるので、ここを普通に超えます。 試したこと Discord の開発者ドキュメントに載っている ID の例 175928847299117063 を使いました。 docs.discord.com API から返ってくる JSON を想定して、id を数値、id_str を文字列で持たせた文字列 raw を作り、そのまま JSON.parse しています(Node.js v24.11.1)。 const raw = '{"id":175928847299117063,"id_str":"175928847299117063"}'; console.log(JSON.parse(raw)); 結果は下記でした。 { id: 175928847299117060, id_str: '175928847299117063' } 数値で読んだ id だけ、末尾が 063 から 060 になっています。 エラーも警告も出ないので、比較して初めて気づきます。 日時だけは合ってしまう ややこしいのは、壊れた数値から日時を取り出しても、ほぼ正しく見えることです。 Discord の Snowflake は、右に 22 ビットずらした値がエポック(2015-01-01 00:00:00 UTC、Unixミリ秒で 1420070400000)からのミリ秒だ。 下記は、壊れた Number と、文字列から作った BigInt の両方で日時を出したものです。 4194304 は 2 の 22 乗で、Number には >> で64ビットのシフトができないので割り算で代用しています。 const s = '175928847299117063'; // id_str(文字列) const EPOCH = 1420070400000; // Discord のエポック const viaNumber = Math.floor(Number(s) / 4194304) + EPOCH; const id = BigInt(s); const viaBigInt = Number(id >> 22n) + EPOCH; console.log(new Date(viaNumber).toISOString()); // 2016-04-30T11:18:25.796Z console.log(new Date(viaBigInt).toISOString()); // 2016-04-30T11:18:25.796Z console.log((id >> 17n) & 0x1fn, (id >> 12n) & 0x1fn, id & 0xfffn); // 1n 0n 7n 日時は両方とも 2016-04-30 11:18:25.796 UTC で、ドキュメントの例と同じになりました。 丸められるのは下位の数ビットだけで、時刻は上位に入っているので、たまたま残るという話です。 ただ、最後の行で取り出しているワーカー番号・プロセス番号・連番(ドキュメントでは 1 / 0 / 7)は下位にあるので、Number 経由だと正しく出ません。 それに、ID 自体が別の値になっているので、データベースで探しても見つからないです。 私のツールも、入力を BigInt にしてからシフトしています。 hashitosystem.com JSON から ID を正しく取り出す いちばん楽なのは、API が文字列でも返している項目(X の id_str など)を使うことです。 Discord の API は最初から ID を文字列で返すので、こちらは数値になる心配がありません。 自分で受け取る JSON が数値しか持っていないときは、JSON.parse の reviver(読み込み中に値を差し替える第2引数の関数)が使えます。 Node.js v24 では、reviver の第3引数 context.source に、数値に変換する前の元の文字列が入っていました。 const raw = '{"id":175928847299117063,"id_str":"175928847299117063"}'; const obj = JSON.parse(raw, (key, value, context) => key === 'id' && context && context.source ? BigInt(context.source) : value ); console.log(obj); // { id: 175928847299117063n, id_str: '175928847299117063' } key は今読んでいる項目名で、id のときだけ元の文字列から BigInt を作っています。 context が無い古い環境では value をそのまま返すので、少なくとも落ちはしません(その場合は丸められた値になります)。 この第3引数は TC39 の提案(JSON.parse source text access)で入ったもので、古いブラウザでは使えないかもしれません。 tc39/proposal-json-parse-with-source BigInt はそのままだと JSON.stringify で例外になるので、保存や送信のときは String(id) で文字列に戻しておくのが無難です。 Snowflake ID は数字に見えても数値として扱わない。 受け取りは文字列、計算は BigInt、と分けておけば、桁が化ける事故は起きません。 --- > 本記事はAI補助で執筆した、個人開発の紹介記事です。

  8. JSON.parse Source Text Access: Stop Losing Precision on Big IDs Your API returns an order ID of 9007199254740993. Your frontend logs 9007199254740992. Nobody touched it. JSON.parse quietly rounded it, because every JSON number becomes a JavaScript double, and doubles only hold integers exactly up to Number.MAX_SAFE_INTEGER (9007199254740991). Anything bigger gets snapped to the nearest representable value before your code ever sees it. The usual workarounds are all a little sad. Ask the backend to send IDs as strings (id_str, if you remember the old Twitter API). Pull in a third-party parser like json-bigint. Or run a regex over the raw response to wrap long numbers in quotes before parsing, and hope nothing in a string field looks like a number. The platform now has a real fix: the JSON.parse source text access proposal, which adds a context argument to revivers and a matching JSON.rawJSON() for serialization. It reached Stage 4 at TC39 in late 2025, and it is already Baseline: Chrome 114, Firefox 135, Safari 18.4, Node 21, Deno 1.33, and Bun 1.1.43 all ship it. The reviver gets a third argument A reviver has always received (key, value). It now receives (key, value, context), and for primitive values context.source is the exact slice of JSON text that produced value. The number was still parsed into a lossy double, but you get the original digits next to it, so you can decide what it should become. const json = '{"orderId": 9007199254740993, "total": 42.5}'; const order = JSON.parse(json, (key, value, context) => { if (key === "orderId") { // value is already rounded; context.source is not return BigInt(context.source); } return value; }); order.orderId; // 9007199254740993n order.total; // 42.5 Keying off property names works when you control the schema, but a generic version is more useful in a shared fetch helper. Promote only integers that actually lost precision, and leave everything else alone: const INTEGER = /^-?\d+$/; export function preciseReviver(key, value, context) { if ( typeof value === "number" && !Number.isSafeInteger(value) && context?.source && INTEGER.test(context.source) ) { return BigInt(context.source); } return value; } const res = await fetch("/api/orders"); const data = JSON.parse(await res.text(), preciseReviver); Two details there matter. Response.json() doesn't accept a reviver, so you read the body as text and parse it yourself. And the optional chaining on context?.source is deliberate: context.source only exists for primitives. When the reviver is called for an object or array, source is absent, because there's no single meaningful slice of text to give you once the children have already been revived. Writing it back out with JSON.rawJSON Parsing is half the problem. Hand that BigInt to JSON.stringify and you get TypeError: Do not know how to serialize a BigInt. Convert it to a Number first and you're back to rounding. JSON.rawJSON() closes the loop: it wraps a piece of JSON text that stringify will emit verbatim. function preciseReplacer(key, value) { return typeof value === "bigint" ? JSON.rawJSON(value.toString()) : value; } JSON.stringify({ orderId: 9007199254740993n }, preciseReplacer); // '{"orderId":9007199254740993}' The output is a real JSON number, not a quoted string, so a Go or Java backend expecting an int64 reads it without any schema change. JSON.rawJSON is strict about what it accepts: the text must be valid JSON for a primitive, so JSON.rawJSON('[1, 2]') or a string containing a raw line break throws a SyntaxError. The returned object is frozen with a null prototype, and JSON.isRawJSON() tells you if you're holding one. Decimals, not just big integers Money is the other place this pays off. 0.1 parsed as a double is not exactly one tenth, and a payments API that returns "amount": 19.99 hands you a value that's already slightly wrong before your decimal library sees it. With context.source you can feed the original digits straight into whatever arbitrary-precision type you use: import Decimal from "decimal.js"; const invoice = JSON.parse(body, (key, value, context) => key === "amount" ? new Decimal(context.source) : value, ); On the way out, JSON.rawJSON(amount.toFixed(2)) writes 19.99 as an unquoted number again. When not to reach for it A reviver runs once per value in the document, so it is measurably slower than a plain JSON.parse on large payloads. If your IDs are already strings, or every number fits in the safe range, you don't need any of this. In TypeScript, older lib definitions may not know about the third reviver argument or JSON.rawJSON yet, so you might need a small ambient declaration until your lib setting catches up. For older browsers, core-js ships a polyfill for both halves. The bigger change is where the fix lives. Precision used to be something you negotiated with the backend team or patched with a dependency. Now it's a few lines in the one fetch helper every request already goes through. MDN's pages on JSON.parse() and JSON.rawJSON() have the full reference, and the TC39 proposal covers the design reasoning.

    1 reaction
  9. I want bun to have struct support. So here a draft into Webkit. With bun looking to the future and the need the support for high preforming JavaScript. Maybe if Bun adopts then this could help push the spec forward WebKit#904 Stage 2 Proposal tc39/proposal-structs I hope @jarredsumner still see the need for underlining support

    1 like · 50 views
  10. @jarredsumner Could we have a private APIs for structs tc39/proposal-structs

    4 likes · 895 views

Achievements

Awards earned by the repositories tc39 owns. Select an award to embed its badge.

No awards yet

Repositories earn awards by reaching the top three in a completed trending day or week.

A repository earns an award for finishing first, second or third in a completed trending day or week, ranked by new stars, new forks or pull request authors. Awards follow a repository through renames and transfers and count toward its current owner. Weeks run Monday to Sunday, UTC.