Dart 3.13 Release Highlights: What Shipped Beyond Primary Constructors
A tour of the Dart 3.13 changelog beyond the headline feature: new Isolate APIs for synchronous execution and event loop control, Future.pause, typed unmodifiable collections, bit-counting getters on int, breaking type promotion rules, and the analyzer and formatter changes you need to plan for.
Published on • October 10, 2026
AI Assistant

Dart 3.13 landed on 2026-08-12, and almost every write-up about it leads with primary constructors. That is fair — the feature is the reason the release bumped the language version — but it is a fraction of what changed. The same release quietly added a new concurrency API surface, two library methods that will change how you type collections, a genuinely useful pair of bit-twiddling getters, and a breaking fix to type promotion that can change whether your code compiles after an SDK upgrade.
This post walks the changelog entry by entry, separating the things you will use daily from the things that will break your build, and shows you exactly what to do about each one.
In this tutorial, you will learn how to:
- Read the Dart changelog like a release engineer rather than a news reader
- Use the new
IsolateAPIs (Isolate.runSync,Isolate.create,Isolate.shutdownSync) for synchronous and event-loop-controlled work - Replace
List.unmodifiableandMap.unmodifiablewith the stronger-typedunmodifiableOfconstructors - Count bits efficiently with
int.trailingZeroBitCountandint.oneBitCount - Handle the breaking type promotion change and the
final/varparameter restriction - Adopt the new
no_raw_typesandno_dynamic_castslint rules in place of analysis options
Key technologies: Dart 3.13, dart:isolate, dart:core, dart:async, dart:io, dart format, dart fix, analysis_options.yaml.
Prerequisites
- Dart SDK 3.13 or later (
dart --version) - A Dart or Flutter project with an
analysis_options.yaml - Familiarity with
dart analyze,dart fix, anddart format
Where to look first: the changelog, not the blog post
The announcement blog post tells you what the Dart team is excited about. The changelog tells you what will actually stop compiling. Every entry there is tagged with one or more of New, Fixed, Breaking, Changed, Removed, and — critically — Language versioned.
That last tag is the one that matters for a gradual upgrade. A language-versioned change only takes effect once your package’s language version (the SDK constraint in pubspec.yaml) reaches 3.13. This is why a monorepo can have one package adopting primary constructors while its neighbour still formats under the old rules.
The practical workflow is:
# 1. See what the SDK itself reports
dart --version
# 2. Dry-run the automated migrations
dart fix --dry-run
# 3. Apply them
dart fix --apply
# 4. Reformat under the new language version
dart format .
dart fix can bulk-apply several of the 3.13 lint violations for you, which is why running it before reading the rest of this post is worth the two minutes.
The language changes
Primary constructors (the headline)
Dart 3.13 adds primary constructors: a way to declare a constructor and its instance fields together in the class declaration header. This is the feature that defines the release, and it requires a language version of 3.13 or later.
class Point {
const Point(this.x, this.y);
final double x;
final double y;
}
becomes:
class Point(double x, double y);
Because this is the release’s signature feature, it has its own deep-dive in the changelog and the language docs. This post focuses on the rest of the release, which is where most teams will spend their migration time.
final and var are reserved on non-declaring parameters
This is the single most likely source of compile errors when you bump to 3.13. You can no longer write final or var on parameters that do not declare a field. Dart now reserves both modifiers for primary constructor parameters.
// Before 3.13: legal, but now an error.
void upload(final String path) { ... }
// After 3.13: strip the modifier.
void upload(String path) { ... }
If your intent was to stop the parameter being reassigned inside the body, the replacement is the parameter_assignments lint rule, which flags reassignment instead of forbidding it at the signature:
linter:
rules:
- parameter_assignments
Run dart fix --apply — this migration is mechanical and the tool handles it.
Type promotion rules changed (breaking)
Dart 3.13 also ships a breaking fix to type promotion to eliminate unsound behaviour (tracked in sdk#62889). Type promotion is the compiler feature that lets you treat a nullable value as non-null after a check:
String? maybeName = readName();
if (maybeName != null) {
print(maybeName.length); // promoted to String
}
When promotion rules tighten, code that previously compiled with an implicit downcast or an unsound promotion can stop compiling. The fix is almost always explicit: introduce a local variable inside the check, or add an explicit cast where you genuinely know the type.
Because this entry is tagged Breaking but not Language versioned, it applies as soon as you upgrade the SDK. Budget a full dart analyze pass before you upgrade a shared SDK in CI.
The concurrency upgrade: dart:isolate
This is the most underrated part of 3.13. dart:isolate gains a set of synchronous execution and event loop control APIs:
Isolate.runSyncIsolate.createIsolate.shutdownSyncIsolate.pinToCurrentThreadIsolate.isPinnedToCurrentThreadIsolate.runEventLoopSyncIsolate.onEventIsolate.handleEvent
Isolate.runSync is the one to learn first. Isolate.run has long been the idiomatic way to move a CPU-bound function off the main isolate without a manual ReceivePort dance:
final digest = await Isolate.run(() => sha256(bytes));
Isolate.runSync gives you the same escape hatch when you need the result before the current turn of the event loop completes. That is a narrow use case — blocking the calling isolate is exactly what the async version exists to avoid — but it is the right tool for CLI entry points, test setup, and synchronous plugin initialization where there is no event loop turn to await.
The thread-affinity APIs (Isolate.pinToCurrentThread, Isolate.isPinnedToCurrentThread) matter if you are integrating with FFI or a native library that requires calls to arrive on the same OS thread. Previously you had to encode that constraint manually; now it is part of the isolate’s contract.
Isolate.onEvent and Isolate.handleEvent close the loop, giving you explicit control over how an isolate drains its event queue. Combined with Isolate.runEventLoopSync, this is the first release where Dart gives you a real story for deterministic isolate behaviour instead of “it runs when it runs.”
dart:core: typed unmodifiable collections
List.unmodifiable and Map.unmodifiable return List<E> and Map<E>, but their static types have always been looser than they look. 3.13 adds List.unmodifiableOf and Map.unmodifiableOf, which provide stronger static typing:
// Before
final List<int> a = List.unmodifiable([1, 2, 3]);
// After: the type flows through the constructor
final a = List.unmodifiableOf([1, 2, 3]);
The win shows up most clearly when you are handing a collection to code that needs to prove something about its element type at compile time. Reach for the Of variants in new code and in APIs where a signature mismatch would otherwise force a cast.
dart:core: bit counting on int
Two new getters make a common low-level task both faster and more readable:
int.trailingZeroBitCount— the count of trailing zero bits (ctz)int.oneBitCount— the population count, or number of set bits (popcount)
final mask = 0b10110000;
print(mask.trailingZeroBitCount); // 4
print(mask.oneBitCount); // 3
The performance note in the changelog is important: on native platforms these operate on the full 64-bit two’s complement representation, while on web targets they operate on the least significant 32 bits (tracked in sdk#52673). If you are writing code that must behave identically on Flutter web and mobile, mask your values to 32 bits before calling these getters.
Before 3.13 you would have written a loop or hand-rolled a bit-twiddling trick. This is exactly the kind of API that removes a whole category of subtle bugs.
dart:async: Future.pause
Future.pause arrives as an alternative to Future.delayed when you do not have a callback:
await Future.delayed(const Duration(seconds: 2)); // schedules a timer
await Future.pause(const Duration(seconds: 2)); // pauses instead
The distinction is that pause suspends the future’s completion rather than scheduling work on the timer queue. For rate limiters and retry backoffs where you are already holding a future and simply want to delay its resolution, pause expresses intent more precisely and avoids creating a timer you do not need.
dart:io changes you should know about
Three entries here have real consequences:
Cookie date parsing is permissive again (fixed). The parser now follows the permissive algorithm in the HTTP state management RFC and accepts non-standard date formats rather than rejecting them. If you have a workaround for strict parsing, remove it.
File timestamps preserve microsecond precision (behavioural change). File.lastModified, FileStat, lastAccessed, lastModified, setLastAccessed, and setLastModified no longer truncate to milliseconds (sdk#42444). Any test that asserts an exact timestamp rounded to milliseconds will now fail. The fix is to compare with a tolerance, or to round explicitly in the assertion.
NetworkInterface.addresses returns List<InterfaceAddress> (breaking). A new InterfaceAddress subtype of InternetAddress exposes prefixLength and a broadcast getter. Classes that implement NetworkInterface and override addresses must update their return type (sdk#63216).
Also worth noting: InternetAddress.lookup no longer accepts the non-standard IPv4 formats historically tolerated by inet_aton. If you have been passing shorthand addresses like 10.1 through to DNS lookups, normalise them first.
Analyzer and linter: two rules replace two analysis options
Two new lint rules land as replacements for existing analysis_options.yaml settings:
| Old analysis option | New lint rule |
|---|---|
strict-raw-types | no_raw_types |
strict-casts | no_dynamic_casts |
Both are described as replacing their predecessors for “more consistent type checking.” If your analysis_options.yaml still sets strict-raw-types or strict-casts, plan a migration to the rule-based equivalents so your configuration matches what the analyzer is actually enforcing.
Several new lints also ship, and they pair naturally with the primary constructor feature:
use_declaring_parameters— encourages declaring parameters in primary constructorsunnecessary_primary_constructor_body— flags empty or unnecessary primary constructor bodiesunnecessary_type_name_in_constructor— flags redundant explicit type names in constructorsasync_return_with_no_await— warns when anasyncfunction returns a non-Futurevalue withoutawaitempty_container_bodies— flags empty bodies in classes, enums, mixins, and extensionsinitialize_in_field_declaration— recommends initializing fields at their declaration siteunnecessary_const_in_enum_constructor— flags redundantconstin enum constructors
Two low-value rules are deprecated: avoid_private_typedef_functions and one_member_abstracts. If you relied on them, you will need to reimplement them as an analyzer plugin.
An experimental use_primary_constructors rule is also available for teams that want the analyzer to nudge them toward adopting the syntax.
Tooling: formatter, pub, and the CLI
The formatter received a large batch of language-versioned changes in 3.13. These only apply at language version 3.13+, which means upgrading will reformat parts of your codebase even if you change nothing else. The notable ones:
- Block formatting parameter lists
- Block formatting of
as,is, andis!expressions - Imports separated into distinct sections
- Call chains split preferentially for single-element targets
- Trailing commas added when splitting extension type representation clauses
- Blank lines enforced around mixin and extension type declarations without a
;body if-caseguards split when the associated pattern block-splits
Expect a large one-time diff on the first dart format . after upgrading. Run it in its own commit with no other changes.
On the pub side:
dart pub workspace listlists all workspace packages and their directory paths, with--jsonoutput — a genuine quality-of-life win for monoreposdart pub cache preloadinstalls packages directly intoPUB_CACHEfrom.tar.gzarchives, which is useful for air-gapped CI
On the CLI side, dart build cli gains cross-compilation via --target-os and --target-arch. If you ship Dart CLIs for multiple platforms from one build machine, this removes a whole class of build orchestration.
Two entries to watch in the runtime:
- Built-in fallback root certificates are removed when system certificates are missing. Provide them explicitly with
--root-certs-fileor--root-certs-cache. Dart_FileModifiedCallbacknow passes microseconds since epoch instead of milliseconds — a breaking change for C embedders.
A suggested upgrade order
Do not upgrade and fix everything in one pass. A sequence that works:
- Upgrade the SDK, then run
dart analyze. Read only theBreakingfailures first — type promotion and the parameter modifier rules are the usual culprits. - Run
dart fix --applyto clear the mechanical migrations. - Update
analysis_options.yaml— swapstrict-raw-typesforno_raw_typesandstrict-castsforno_dynamic_casts, and enable the new lints you want. - Run
dart format .in an isolated commit. The formatter changes are language-versioned, so they land as soon as your SDK constraint reaches 3.13. - Audit
dart:iotimestamp tests for millisecond assumptions, and update anyNetworkInterfaceimplementations. - Adopt primary constructors incrementally — enable
use_declaring_parametersand let the analyzer drive the conversion file by file.
Wrapping up
Dart 3.13 is a language-versioned release with a long tail. Primary constructors are what gets the headline, but the migrate-or-break items are the parameter modifier restriction and the type promotion fix, and the most useful day-to-day additions are the new Isolate APIs, the unmodifiableOf constructors, and the bit-counting getters.
Read the changelog, run dart fix --dry-run, and treat the formatter diff as its own commit. That is the whole migration.