Skip to content
Blog

Primary Constructors in Dart: The Design Decisions Behind the Syntax

Dart 3.13 ships primary constructors after years of debate. Walk through the trade-offs the language team weighed and what they mean for your code.

Published on • October 16, 2026

AI Assistant

You already know how to write class Point(var int x, var int y); and that it means the same thing as the five-line version you wrote last year. What is worth your time is why the syntax looks the way it does, because those decisions now shape every class you write in Dart 3.13. The design went through several rejected alternatives, a deliberate bundling of two features, and one companion syntax change (new as a constructor keyword) that most people noticed second.

Why Dart adds sugar at all

Language features are not free. Bob Nystrom’s design essay compares complexity in a language to weight in an airplane: some is necessary, but weight without a job risks the whole apparatus never leaving the ground. Sugar adds no capability, and it pushes a cost onto users too: from then on you pick a syntax every time you express something.

So the Dart team only justifies sugar in a few cases:

  • The new way is simply better than the old way. Dropping the mandatory new keyword in Dart 2.0 is the template: the old syntax stayed for compatibility, the unnecessary_new lint nudges you off it, and dart fix strips the keywords for you.
  • The sugar makes a common case dramatically shorter. Enums are the exhibit: enum Color { red, blue, yellow } unpacks into what used to be Josh Bloch’s typesafe enum pattern: a class with static constants and a private constructor.
  • The sugar makes intent legible. A class with a public constructor and three static constants does not tell a reader whether the set of values is closed. An enum declaration does. Better syntax raises semantics to a higher abstraction level, reducing reading cost even though the language grew.

Primary constructors land mostly in the second and third categories.

The actual problem

For years the #1 open issue on the Dart language repo was a request for data classes. Read the comments and most people did not care about value semantics (equality and hash code); they wanted an easier way to define a class that has a constructor and stores state. That capability is a Kotlin primary constructor, an idea the Dart blog traces back to Scala, with C# and Java adding their own takes since.

Before Dart 3.13 you had three increasingly terse options:

// 1. Full boilerplate: class name twice, types twice, names four times.
class Point {
  final int x;
  final int y;
  Point(int x, int y) : x = x, y = y;
}

// 2. Initializing formals: better, still repetitive.
class Point {
  final int x;
  final int y;
  Point(this.x, this.y);
}

// 3. Dart 3.13 primary constructor.
class Point(final int x, final int y);

Option 3 is not more expressive. It is the same runtime behavior with less text and, more importantly, with the field-and-constructor pairing visible in one place.

The alternative that lost: infer the constructor from fields

Swift goes the other direction: you declare fields and get a memberwise initializer for free. The Dart team investigated that direction (issue #698 on dart-lang/language) and rejected it, for a reason worth internalizing: the constructor is usually public API, so you want full control of the signature. The name, const-ness, which parameters are positional versus named, their order, and their default values are all things a library author must choose, not infer.

That fixes the direction: parameters are hand-authored, and exactly one thing remains for the user to say. Should the parameter induce a final field, a mutable one, or no field at all? Hence a leading final or var:

class Point(final int x, final int y); // induces two final fields.
class Counter(var int value);           // induces one mutable field.
class User(String name);                // no modifier, no field.

This mirrors Kotlin’s val and var. The absence of a modifier means the parameter behaves exactly like a traditional constructor parameter: useful when the value is only consumed to initialize something else.

Bundling declaring parameters with the header

Only a primary constructor gets declaring parameters; a proposal to allow var/final parameters in in-body constructors was worked on and dropped. The reasoning: if any constructor anywhere in the body could implicitly create fields, finding all the state a class stores becomes hard. With a primary constructor, the state is always at the top of the class.

That bundling is a real constraint, and the team usually tries not to force users to take two behaviors when they want one (class modifiers deliberately offer both final and sealed). Here they accepted the trade because declaring parameters are themselves sugar: if you do not want a primary constructor, you give up brevity and nothing else.

Avoiding syntactic cliffs

Suppose primary constructors had no bodies. Your class starts as a clean header, grows to ten parameters, and one day you want one line of logging in the constructor. Now you fall off the “syntactic cliff”: a small semantic change forces a large textual rewrite, and code review turns into noise.

Dart’s answer is the this initializer block, which gives a primary constructor an initializer list and/or a body without giving up the header:

class FormatterOptions(
  final int indent = 0,
  final int pageWidth = 80,
) {
  this : assert(indent >= 0) {
    log.write('Created options.');
  }
}

Kotlin solves the same problem with an initializer block in the class body; Dart’s version hangs off this so it is clearly the primary constructor’s. It is also the right spot for a doc comment aimed at the constructor, since a comment above the class header documents the whole class.

Two scopes you have to keep straight

Primary constructor parameters live in two scopes with different meanings:

  • Primary initializer scope: non-late field initializers and the initializer list (this : ...) see the parameter.
  • Primary parameter scope: inside the body block, a declaring parameter’s name refers to the induced field; a non-declaring parameter still refers to the parameter.
class ScopingDemo(var String x, String suffix) {
  final String fieldAtDeclaration = x; // parameter x
  this : fieldInInitializer = x {      // parameter x again
    x = x.toUpperCase();               // field x in the body
    print('$x$suffix');                // suffix is not a field
  }
}

That consistency makes refactoring between the two constructor styles safe. Parameters are also read-only in the initializer scope: assigning to x there is a compile-time error, as is initializing a field twice.

What else changed with the header

A few more decisions fall out of the same philosophy:

  • Empty bodies can be ;. Primary constructors made empty bodies common, so class Point(var int x, var int y); is legal.
  • const moves to the header. class const ConstPoint(final int x, final int y): every field final and definitely initialized, initializer list allowed, body block not.
  • Named primary constructors exist. class Point.custom(var int x, var int y);, plus the private class Point._(...) pattern for forcing callers through factories.
  • Super parameters compose. class Employee(super.name, super.age, final String role) extends Person;
  • Enums get the same treatment, and enum primary constructors are implicitly constant: enum Color(final String hex) { red('#FF0000'), ... }.
  • No other non-redirecting generative in-body constructors. Every generative constructor must redirect to the primary one; factories are unrestricted.

The second syntax change: new and factory as keywords

Dart 3.13 also stopped requiring the class name in in-body constructor declarations. Stroustrup chose class-name constructors to avoid new keywords and admitted the choice “may have been overly clever.” Dart’s grammar already breaks the declaration-mirrors-call-site idea (getters, operators, named parameter braces), and the rule creates a genuine edge case for the static extension members on typedefs that the team wants to support next: which name do you use, the alias or the class behind it?

class LongClassName {
  new();                // unnamed generative constructor
  new create();         // named constructor
  factory() { ... }     // unnamed factory
  factory create() { ... }
}

class SomeClass {
  const new() { ... }   // const generative; reads odd, means "declare a const constructor"
}

Unless your class name is shorter than three characters, the new form is always shorter and more regular.

When to reach for which constructor

  • Prefer a primary constructor for data holders, configuration objects, and anything where the fields are exactly the constructor parameters. It keeps the state inventory in one visible place.
  • Prefer an in-body constructor when the header is already crowded (type parameters plus extends, with, implements, per-parameter doc comments), when none of several constructors is really “primary”, or when the only generative constructor should be private and you do not want a private name in the highly visible header.
  • Reach for late or external fields and you are back to declaring state in the body.

Migration notes

Bumping to 3.13 is where you meet the breaking changes:

  • final and var on formal parameters anywhere except a primary constructor are now errors (extraneous_modifier). Top-level functions, methods, closures, and in-body constructors all count. If you wanted immutable parameters as style, use the parameter_assignments lint instead.
  • While you are still on language version 3.12 or lower, enable avoid_final_parameters and var_with_no_type_annotation to scrub those modifiers ahead of the upgrade.
  • Declaring parameters cannot be late or external, cannot collide with a method or field name, and covariant is only allowed on a var declaring parameter.
  • A this block cannot be async, async*, sync*, or an arrow body, and you get at most one per class.
  • Watch the factory edge case: a method named factory with no return type parses as a factory constructor. Give it a return type (void factory() {}).
  • In a mixin class, a primary constructor must be trivial: no parameters, no initializer list, no body.
dart fix --apply
dart analyze
dart format

Primary constructors do not change what your program does at runtime. They change what a reader can see at a glance, and that is exactly the kind of weight a language can afford to add.

Key Takeaways

  • Primary constructors are sugar with no runtime effect: parameters with var/final induce and initialize fields; a bare parameter does not.
  • Parameters are hand-authored on purpose, because the constructor is public API; Swift-style field-to-constructor inference was investigated and rejected.
  • The this { ... } initializer block exists to prevent syntactic cliffs, so a class can start primary and stay primary as it grows.
  • Only the header form gets declaring parameters; in-body constructors remain fully supported and are the right call for cluttered headers or non-primary constructors.
  • Dart 3.13 also lets you declare constructors with new and factory instead of the class name, and reserves final/var on parameters exclusively for declaring parameters.
  • Prepare with avoid_final_parameters and var_with_no_type_annotation, then run dart fix when you bump the language version.

References: