skip to content

In Dart 3.13, what do primary constructors and the concise new and factory constructor forms change, and what breaks when a package adopts language version 3.13?

level: seniorimportance: nice to knowfreq 20%

answer

  1. constructor and fields in the header
  2. var or final makes a field
  3. this block for list and body
  4. new name(...) inside the body
  5. final on ordinary parameters now errors

basics

~20 s

Dart 3.13 lets a class header declare fields and the main constructor, as in class Money(final int cents, final String currency);, and lets body constructors use new or factory without the class name. Adopting it makes final on ordinary parameters an error.

solid answer

~40 s

Primary constructors are a brevity feature with no new semantics. `class Money(final int cents, final String currency);` declares two final fields and the unnamed constructor that sets them; only parameters marked `var` or `final` induce fields. An initializer list or body goes in a `this : ... { ... }` block inside the class, `class const Money(...)` makes it const (with no body block), and `class Money._(...)` names it. In-body constructors can drop the class name: `new(...)`, `new origin()`, `factory parse(...)`. Upgrading the SDK constraint to `^3.13.0` has costs: `final` or `var` on any other parameter becomes an `extraneous_modifier` error, a class with a primary constructor cannot also have non-redirecting generative in-body constructors, and a method named `factory` with no return type now parses as a constructor.

code

dart · 16 lines
dart
class Money._(final int cents, final String currency) {
  this : assert(cents >= 0), assert(currency.length == 3);

  factory parse(String text) {
    final parts = text.trim().split(' ');
    if (parts.length != 2) throw FormatException('bad money', text);
    return Money._((double.parse(parts[0]) * 100).round(), parts[1]);
  }

  new euros(int cents) : this._(cents, 'EUR');
}

void main() {
  print(Money.parse('4.20 EUR').cents); // 420
  print(Money.euros(99).currency); // EUR
}

go deeper

for a junior

Recall that Dart 3.13 can declare a class's fields and main constructor in its header with var or final parameters.

for a middle

Explain declaring versus ordinary parameters, the this block for initializer lists and bodies, const placement, and the concise new and factory forms.

for a senior

Plan the upgrade: raise the SDK constraint, clear extraneous_modifier errors with dart fix, then convert value types where the header reads better.

for a principal

Set a codebase convention for when primary constructors are preferred, so mixed styles do not become an inconsistent patchwork.

## What the feature is Dart 3.13, released on 2026-08-12, added **primary constructors**. The release notes call it a **brevity feature**: it adds no new runtime semantics, only a shorter way to write a class whose main constructor mirrors its fields. It is enabled per package by the **language version**, i.e. an SDK constraint lower bound of `3.13` (`sdk: ^3.13.0`). ```dart // Before class Money { final int cents; final String currency; const Money(this.cents, this.currency); } // Dart 3.13 primary constructor class const Money(final int cents, final String currency); ``` ## The pieces - **Declaring parameters.** A header parameter marked `var` or `final` induces an instance field of the same name. A parameter without either is an ordinary parameter and declares no field. - **The `this` block.** An initializer list and/or body lives in the class body as `this : assert(cents >= 0) { ... }` or, list only, `this : assert(cents >= 0);`. A class has at most one such block. - **Scopes.** In non-late field initializers and in the initializer list, a header name refers to the **parameter**; inside the body block, a declaring parameter's name refers to the **field**. - **`const`.** Written before the class name: `class const Money(...)`. A const primary constructor cannot have a body block — an initializer list ending in `;` only. - **Named primary constructors.** `class Money._(final int cents, final String currency);` makes the primary constructor private, a common base for public factories. - **Super parameters** work as usual: `class Price(super.cents, super.currency, final double taxRate) extends Money;` - An empty class body `{}` can be replaced by `;`. ## Concise in-body constructors Independently of primary constructors, 3.13 lets constructors in the body omit the class name: | Traditional | Concise | |---|---| | `Money(this.cents, this.currency);` | `new(this.cents, this.currency);` | | `Money.zero() : cents = 0, currency = 'EUR';` | `new zero() : cents = 0, currency = 'EUR';` | | `const Money.euro(int c) : this(c, 'EUR');` | `const new euro(int c) : this(c, 'EUR');` | | `factory Money.parse(String s) { ... }` | `factory parse(String s) { ... }` | Note there is **no dot** between `new` and the name. ## Restrictions 1. A class with a primary constructor **cannot declare other non-redirecting generative constructors** in its body (`non_redirecting_generative_constructor_with_primary`); extra constructors must redirect to it or be factories. This guarantees the primary constructor runs for every instance. 2. A primary constructor **cannot itself redirect**. 3. Declaring parameters **cannot be `late` or `external`**; declare such fields in the body. 4. Header parameters are **read-only** in initializers and the initializer list. 5. A field cannot be initialised **twice** — at its declaration and by the primary constructor. 6. The `this` block cannot be `async`, `async*`, `sync*` or use `=>`. ## What breaks on upgrade - **`final`/`var` on ordinary parameters.** Because these modifiers now mark declaring parameters, writing `void log(final String msg)` in a function, method, closure or ordinary constructor is a compile-time error, `extraneous_modifier`. `dart fix` removes them; teams that wanted immutable parameters switch to the `parameter_assignments` lint. On 3.12 or lower, the `avoid_final_parameters` and `var_with_no_type_annotation` lints find the modifiers before the upgrade. - **A method called `factory` with no return type** (`factory() {}`) now parses as a concise factory constructor; give it an explicit return type. ## Adopting it sensibly The feature pays off most on small value types, data carriers and enhanced enums. For classes with complex invariants, a traditional constructor with an explicit initializer list can stay clearer. Mixed styles are legal, so migration can be incremental: raise the language version, fix the `extraneous_modifier` errors, then convert classes where the header genuinely reads better.

  • In Dart 3.13, how do you add a second generative constructor to a class that has a primary constructor?
    Make it redirect to the primary constructor, for example `new euros(int cents) : this._(cents, 'EUR');`, or make it a factory. A non-redirecting generative in-body constructor is an error when a primary constructor exists, because the language guarantees the primary constructor runs for every instance.
  • In Dart 3.13, how do you give a primary constructor an assert or a body?
    Add a `this` block inside the class: `this : assert(cents >= 0);` for an initializer list only, or `this : assert(cents >= 0) { ... }` with a body. A const primary constructor may have the initializer list but no body block, and the block cannot be `async` or use `=>`.

saying these in an interview costs you the question

  • Every primary constructor parameter becomes a field, with or without var or final
  • Primary constructors add new runtime semantics such as automatic equality
  • A class with a primary constructor can freely add other generative constructors
  • Upgrading to language 3.13 leaves final on ordinary parameters as a harmless style choice
  • Concise named constructors are written new.origin() with a dot