Variables & Types
var, final, and const
let mutableValue = 42;
mutableValue = 100; // let is mutable
const maxValue = 100; // const is immutable at runtime
console.log(mutableValue);
console.log(maxValue); void main() {
var mutableValue = 42;
mutableValue = 100; // var is mutable
final maxValue = 100; // final cannot be reassigned
// maxValue = 200; // compile error
const pi = 3.14159; // const is a compile-time constant
print(mutableValue);
print(maxValue);
print(pi);
} Dart has three levels of immutability:
var (mutable, type inferred), final (cannot be reassigned after initialization, like JavaScript's const), and const (compile-time constant — the value must be known at compile time). JavaScript's const is closest to Dart's final.Type inference
let count = 42; // inferred as number
let greeting = 'hello'; // inferred as string
let items = [1, 2, 3]; // inferred as Array
// count = 'oops'; // no error — JS is dynamic
console.log(count);
console.log(greeting); void main() {
var count = 42; // inferred as int
var greeting = 'hello'; // inferred as String
var items = [1, 2, 3]; // inferred as List<int>
// count = 'oops'; // compile error — type is fixed
print(count);
print(greeting);
print(items);
} Dart infers types from
var declarations and enforces them statically. Once Dart infers count as int, assigning a String is a compile error. JavaScript is dynamic — types can change at runtime without error.Explicit type annotations
// JavaScript has no built-in type syntax
let count = 0;
let name = 'Alice';
let price = 9.99;
let isActive = true;
console.log(count, name, price, isActive); void main() {
int count = 0;
String name = 'Alice';
double price = 9.99;
bool isActive = true;
print(count);
print(name);
print(price);
print(isActive);
} Dart has a rich set of built-in types:
int, double, num (either), String, bool, List, Map, Set, and Null. Explicit annotations are optional when inference works but improve readability for complex types.dynamic — the escape hatch
// All JavaScript variables are effectively dynamic
let value = 42;
value = 'now a string';
value = [1, 2, 3];
console.log(value); void main() {
dynamic value = 42;
value = 'now a string'; // no compile error
value = [1, 2, 3]; // still no error
print(value);
// Type check at runtime — no compile-time safety
if (value is List) {
print('got a List with ${value.length} items');
}
} The
dynamic type opts out of Dart's static type checking — it behaves like a JavaScript variable. Use it sparingly (e.g., when deserializing JSON of unknown shape). Prefer Object? when you want to accept any value while keeping some type safety.late — deferred initialization
// JavaScript — variables are undefined until assigned
let username;
username = 'alice';
console.log(username); void main() {
late String username; // declared but not yet initialized
username = 'alice'; // initialized before use
print(username);
} The
late keyword tells Dart "I promise this will be initialized before it's read." The compiler skips the null-initialization check. At runtime, reading a late variable before assignment throws a LateInitializationError. It's useful for instance fields that can't be set in the constructor.int, double, and num
// JavaScript has one Number type (64-bit float)
let age = 25; // same as 25.0 internally
let price = 9.99;
let total = age + price;
console.log(total); void main() {
int age = 25; // whole numbers only
double price = 9.99; // floating point
num flexible = 42; // either int or double
flexible = 3.14; // reassigning to double is fine
print(age + price); // promotes to double: 34.99
print(flexible);
} Dart distinguishes between
int (arbitrary precision integers) and double (64-bit floating point). Use num when a value can be either. JavaScript uses 64-bit floats for all numbers, so 1 and 1.0 are the same — in Dart, int and double are distinct types.Null Safety
Nullable vs. non-nullable types
let name = null; // any variable can be null
let count; // undefined — also falsy
console.log(name);
console.log(count); void main() {
String nonNullable = 'Alice'; // can never be null
String? nullable = null; // ? suffix allows null
// nonNullable = null; // compile error!
nullable = 'Bob';
print(nonNullable);
print(nullable);
} Dart's null safety is "sound" — by default, no type can hold
null. Add ? to opt in: String? can be null, String cannot. The compiler enforces this at compile time, so null dereference errors are caught before the program runs. JavaScript has no equivalent protection.Null assertion with !
function maybeGetText() { return 'hello'; }
let text = maybeGetText();
// Crashes at runtime if text is null/undefined:
console.log(text.length); void main() {
String? maybeText = 'hello';
// ! asserts non-null; throws if null at runtime
print(maybeText!.length);
// Safer: check first (smart cast)
if (maybeText != null) {
print(maybeText.length); // Dart knows it's non-null here
}
} The
! null assertion operator tells Dart "I know this isn't null, trust me." It throws at runtime if wrong. Prefer if (x != null) or ?. for safer code. Within an if (x != null) block, Dart automatically promotes x to its non-nullable type (called "promotion").Null coalescing with ??
let username = null;
let displayName = username ?? 'Guest';
console.log(displayName);
let city;
console.log(city ?? 'Unknown city'); void main() {
String? username = null;
String displayName = username ?? 'Guest';
print(displayName);
String? city;
print(city ?? 'Unknown city');
} Dart's
?? operator (null coalescing) works identically to JavaScript's ??: if the left side is null, evaluate and return the right side. The type of displayName is String (not String?) because the compiler knows 'Guest' is never null.Conditional access with ?.
let username = null;
let length = username?.length; // undefined if null
console.log(length);
username = 'Alice';
console.log(username?.length); void main() {
String? username = null;
int? length = username?.length; // null if username is null
print(length);
username = 'Alice';
print(username?.length); // 5
} Dart's
?. null-aware access operator works the same as JavaScript's optional chaining ?.: if the receiver is null, the whole expression short-circuits to null. Dart also has ?[] for null-safe index access.Null-aware assignment with ??=
function computeValue() { return 42; }
let cache = null;
cache ??= computeValue(); // assign only if null
cache ??= 99; // already set, skipped
console.log(cache); String computeExpensiveResult() => 'computed value';
void main() {
String? cachedResult;
cachedResult ??= computeExpensiveResult(); // assign only if null
cachedResult ??= 'ignored'; // already set, skipped
print(cachedResult);
} The
??= operator assigns only if the variable is currently null. It's equivalent to x = x ?? value. Both JavaScript and Dart have this operator; in Dart it's a natural fit for lazy initialization with null safety.Strings
String interpolation
const name = 'Alice';
const age = 30;
const greeting = `Hello, ${name}! You are ${age} years old.`;
console.log(greeting);
console.log(`2 + 2 = ${2 + 2}`); void main() {
const name = 'Alice';
const age = 30;
final greeting = 'Hello, $name! You are $age years old.';
print(greeting);
print('2 + 2 = ${2 + 2}'); // ${} for expressions
} Dart uses
'\$variable' for simple variable interpolation and '\${expression}' for expressions — no backtick required. Single-variable interpolation doesn't need braces. JavaScript template literals always use backticks and `${...}` for everything.Multiline strings
const poem = `Roses are red,
Violets are blue,
Dart is typed,
And JavaScript too.`;
console.log(poem); void main() {
const poem = """Roses are red,
Violets are blue,
Dart is typed,
And JavaScript too.""";
print(poem);
// Single-quoted triple strings also work
const note = '''Single quoted
multiline''';
print(note);
} Dart uses triple single-quotes (
''') or triple double-quotes (""") for multiline strings. Both varieties support interpolation. JavaScript uses template literals (backticks) for multiline strings.Common string methods
const message = ' Hello, World! ';
console.log(message.trim());
console.log(message.trim().toLowerCase());
console.log(message.includes('World'));
console.log(message.trim().startsWith('Hello'));
console.log(message.trim().split(', ')); void main() {
const message = ' Hello, World! ';
print(message.trim());
print(message.trim().toLowerCase());
print(message.contains('World')); // contains, not includes
print(message.trim().startsWith('Hello'));
print(message.trim().split(', '));
} Dart's string API is similar to JavaScript's but with some name differences:
contains() instead of includes(). All Dart String instances are immutable. Dart strings are UTF-16 sequences, the same as JavaScript.Raw strings
const path = String.raw`C:\Users\alice\docs`;
console.log(path);
const pattern = '\\d+'; // regex digit pattern
console.log(pattern); void main() {
final path = r'C:\Users\alice\docs'; // r prefix = raw string
print(path);
final pattern = r'd+'; // backslash is not interpreted
print(pattern);
} The
r prefix before a Dart string literal makes it raw — backslashes are treated as literal characters, not escape sequences. This is especially useful for regular expressions and Windows file paths. JavaScript uses String.raw\`...\` for the same purpose.Collections
List creation and typing
const numbers = [1, 2, 3, 4, 5];
const names = ['Alice', 'Bob', 'Carol'];
console.log(numbers);
console.log(names.length); void main() {
final numbers = [1, 2, 3, 4, 5]; // List<int> inferred
final names = ['Alice', 'Bob', 'Carol']; // List<String> inferred
print(numbers);
print(names.length);
// Explicit type
List<double> prices = [9.99, 14.99, 29.99];
print(prices);
} Dart's equivalent of JavaScript arrays is
List. Dart infers the element type — [1, 2, 3] becomes List<int>. Mixing types produces List<Object>, which loses type safety. By default, Dart lists are growable (like JavaScript arrays).List operations
const fruits = ['apple', 'banana'];
fruits.push('cherry');
fruits.unshift('avocado');
console.log(fruits.includes('banana'));
fruits.splice(fruits.indexOf('banana'), 1);
console.log(fruits); void main() {
final fruits = ['apple', 'banana'];
fruits.add('cherry'); // add to end
fruits.insert(0, 'avocado'); // add at index
print(fruits.contains('banana'));
fruits.remove('banana'); // remove by value
print(fruits);
} Dart's
List API is similar to JavaScript arrays but with clearer names: add() instead of push(), insert() instead of unshift(), and remove() (by value) instead of splice(). Use removeAt(index) to remove by index.map, where, and fold
const numbers = [1, 2, 3, 4, 5, 6];
const doubled = numbers.map(n => n * 2);
const evens = numbers.filter(n => n % 2 === 0);
const total = numbers.reduce((sum, n) => sum + n, 0);
console.log([...doubled]);
console.log(evens);
console.log(total); void main() {
final numbers = [1, 2, 3, 4, 5, 6];
final doubled = numbers.map((number) => number * 2);
final evens = numbers.where((number) => number % 2 == 0);
final total = numbers.fold(0, (sum, number) => sum + number);
print(doubled.toList());
print(evens.toList());
print(total);
} Dart's
where() corresponds to JavaScript's filter(), and fold() corresponds to reduce() with an initial value. Note that map() and where() return lazy Iterables — call .toList() to materialize them into a List.Map creation
const scores = { Alice: 95, Bob: 87, Carol: 92 };
console.log(scores['Alice']);
console.log(Object.keys(scores)); void main() {
final scores = {'Alice': 95, 'Bob': 87, 'Carol': 92}; // Map<String, int>
print(scores['Alice']);
print(scores.keys.toList());
// Explicit type
Map<String, double> prices = {'coffee': 3.50, 'tea': 2.50};
print(prices);
} Dart's
Map uses the same {key: value} literal syntax as JavaScript objects, but keys can be of any type (not just strings). Access returns null for missing keys instead of JavaScript's undefined. Map.keys and Map.values are lazy iterables.Map operations
const inventory = { apples: 5, bananas: 3 };
inventory.oranges = 8;
delete inventory.bananas;
console.log('apples' in inventory);
console.log(inventory); void main() {
final inventory = {'apples': 5, 'bananas': 3};
inventory['oranges'] = 8; // add new key
inventory.remove('bananas'); // remove key
print(inventory.containsKey('apples'));
inventory.update('apples', (count) => count + 1);
print(inventory);
} Dart maps use bracket notation for both get and set. The
remove(key) method returns the removed value (or null). The update() method transforms an existing value — use update(key, fn, ifAbsent: () => default) to also handle missing keys.Set creation — watch the empty literal
const tags = new Set(['dart', 'flutter', 'mobile']);
tags.add('web');
tags.delete('mobile');
console.log(tags.has('flutter'));
console.log([...tags]); void main() {
final tags = <String>{'dart', 'flutter', 'mobile'}; // Set<String>
tags.add('web');
tags.remove('mobile');
print(tags.contains('flutter'));
print(tags.toList());
// Caution: {} alone creates an empty Map, not a Set!
// var empty = {}; // Map<dynamic, dynamic>
var emptySet = <String>{}; // correct empty Set
print(emptySet.runtimeType);
} Dart's
Set literal looks like {"a", "b", "c"}, but {} alone creates an empty Map, not a Set — a common gotcha. Always write <Type>{} for an empty set. Dart sets maintain insertion order, like JavaScript's Set.Collection if and for in literals
const showExtra = true;
const baseItems = ['apple', 'banana'];
const items = [
...baseItems.map(s => s.toUpperCase()),
...(showExtra ? ['CHERRY'] : []),
];
console.log(items); void main() {
const showExtra = true;
final baseItems = ['apple', 'banana'];
final items = [
for (final item in baseItems) item.toUpperCase(),
if (showExtra) 'CHERRY',
];
print(items);
} Dart allows
for and if directly inside collection literals. [for (var x in xs) transform(x)] is a concise alternative to xs.map(transform).toList(). This feature also works inside Map and Set literals. JavaScript has no equivalent built-in syntax.Control Flow
if / else — no truthy coercion
const score = 85;
if (score >= 90) {
console.log('A');
} else if (score >= 80) {
console.log('B');
} else {
console.log('C or below');
}
// JS — truthy coercion: any non-empty string is true
if ('hello') { console.log('truthy string'); } void main() {
const score = 85;
if (score >= 90) {
print('A');
} else if (score >= 80) {
print('B');
} else {
print('C or below');
}
// Dart — conditions MUST be bool; no truthy coercion
const greeting = 'hello';
if (greeting.isNotEmpty) { print('non-empty string'); }
} Dart's
if/else syntax is nearly identical to JavaScript's. The key difference: Dart conditions must be strictly bool — there are no "truthy" or "falsy" values. Writing if (someString) is a compile error; you must write if (someString.isNotEmpty).switch as an expression (Dart 3)
const score = 85;
const grade = score >= 90 ? 'A'
: score >= 80 ? 'B'
: score >= 70 ? 'C'
: 'F';
console.log(grade); void main() {
const score = 85;
final grade = switch (score) {
>= 90 => 'A',
>= 80 => 'B',
>= 70 => 'C',
_ => 'F',
};
print(grade);
// Multi-value arms with ||
const day = 'Saturday';
final kind = switch (day) {
'Saturday' || 'Sunday' => 'Weekend',
_ => 'Weekday',
};
print(kind);
} Dart 3 introduced
switch as an expression that returns a value. The pattern >= 90 is a relational pattern — it matches any value that satisfies the comparison. Arms are evaluated in order. JavaScript has no native switch expression; you'd use nested ternaries or an object lookup.for and for-in loops
const fruits = ['apple', 'banana', 'cherry'];
for (let i = 0; i < fruits.length; i++) {
console.log(`${i}: ${fruits[i]}`);
}
for (const fruit of fruits) {
console.log(fruit);
} void main() {
final fruits = ['apple', 'banana', 'cherry'];
for (int i = 0; i < fruits.length; i++) {
print('$i: ${fruits[i]}');
}
for (final fruit in fruits) {
print(fruit);
}
} Dart's
for (var item in collection) is the equivalent of JavaScript's for (const item of collection). Dart has no for...in for object keys (that's JavaScript-specific) — iterate a Map with map.keys or map.entries.while and do-while
let count = 0;
while (count < 3) {
console.log(count);
count++;
}
let value = 10;
do {
console.log(value);
value--;
} while (value > 8); void main() {
int count = 0;
while (count < 3) {
print(count);
count++;
}
int value = 10;
do {
print(value);
value--;
} while (value > 8);
} Dart's
while and do-while loops are identical in syntax to JavaScript's. Like if, the loop condition must be a bool expression — there is no truthy coercion.Functions
Function definition with types
function add(a, b) { // no type info
return a + b;
}
console.log(add(3, 4));
console.log(add('3', 4)); // type coercion: '34' int add(int a, int b) {
return a + b;
}
// Arrow shorthand for single-expression functions
double multiply(double a, double b) => a * b;
void main() {
print(add(3, 4));
// add('3', 4); // compile error — wrong types
print(multiply(3.0, 4.0));
} Dart functions declare parameter types and return types. Passing the wrong type is a compile error. The
=> shorthand works for single-expression functions. Dart infers return types when omitted, but explicit annotations are idiomatic.Named parameters
function greet({ name, greeting = 'Hello' } = {}) {
console.log(`${greeting}, ${name}!`);
}
greet({ name: 'Alice' });
greet({ name: 'Bob', greeting: 'Hi' }); void greet({required String name, String greeting = 'Hello'}) {
print('$greeting, $name!');
}
void main() {
greet(name: 'Alice');
greet(name: 'Bob', greeting: 'Hi');
} Dart wraps named parameters in curly braces in the function signature — not just at the call site. The
required keyword marks a named parameter as mandatory; omitting it is a compile error. JavaScript achieves named parameters through destructuring, without enforcing which keys are required.Optional positional parameters
function log(message, level = 'INFO') {
console.log(`[${level}] ${message}`);
}
log('Server started');
log('Disk full', 'WARN'); void log(String message, [String level = 'INFO']) {
print('[$level] $message');
}
void main() {
log('Server started');
log('Disk full', 'WARN');
} Square brackets make positional parameters optional in Dart. Callers pass them by position, not by name — unlike named parameters (curly braces). Named and optional positional parameters cannot be mixed in the same function.
Function types and references
function multiplyByTwo(number) { return number * 2; }
const transform = multiplyByTwo;
const numbers = [1, 2, 3];
console.log(numbers.map(transform)); int multiplyByTwo(int number) => number * 2;
void main() {
int Function(int) transform = multiplyByTwo; // typed function reference
final numbers = [1, 2, 3];
print(numbers.map(transform).toList());
} Dart uses
ReturnType Function(ParamType) syntax to express function types. JavaScript has no type annotation syntax in plain JS. Typed function parameters catch mismatches at compile time — passing a String Function(String) where an int Function(int) is expected is a compile error.Arrow functions
const square = (n) => n * n;
const greet = (name) => `Hello, ${name}!`;
const numbers = [1, 2, 3, 4, 5];
console.log(numbers.map(square)); void main() {
int square(int number) => number * number;
String greet(String name) => 'Hello, $name!';
final numbers = [1, 2, 3, 4, 5];
print(numbers.map(square).toList());
print(greet('Alice'));
// Inline anonymous arrow function
print(numbers.where((number) => number > 3).toList());
} Dart's
=> shorthand creates a single-expression function, similar to JavaScript's arrow function syntax. Unlike JavaScript, Dart arrow functions still require type annotations when declared as named functions. Anonymous lambdas can omit types when they're inferable from context.Closures & Higher-Order Fns
Closures capture variables
function makeCounter() {
let count = 0;
return () => {
count++;
return count;
};
}
const counter = makeCounter();
console.log(counter());
console.log(counter()); void Function() makeCounter() {
int count = 0;
return () {
count++;
print(count);
};
}
void main() {
final counter = makeCounter();
counter();
counter();
} Dart closures capture variables from their enclosing scope, just like JavaScript closures. The key difference is that Dart closures are typed:
void Function() explicitly states the closure takes no arguments and returns nothing.Higher-order functions and tear-offs
function applyToAll(numbers, fn) {
return numbers.map(fn);
}
const doubled = applyToAll([1, 2, 3], n => n * 2);
console.log([...doubled]); List<int> applyToAll(List<int> numbers, int Function(int) transform) {
return numbers.map(transform).toList();
}
int doubled(int number) => number * 2;
void main() {
print(applyToAll([1, 2, 3], doubled));
// Tear-off: pass an instance method as a function
final words = ['hello', 'world'];
print(words.map((word) => word.toUpperCase()).toList());
} Dart supports passing functions as arguments with full type safety. A "tear-off" is Dart's term for referencing a method without calling it — writing
someObject.method creates a bound closure. JavaScript uses the same pattern without the type annotations.typedef for named function types
// JavaScript has no typedef
// TypeScript adds: type Predicate<T> = (value: T) => boolean
function filter(items, predicate) {
return items.filter(predicate);
}
console.log(filter([1, 2, 3, 4, 5], n => n > 2)); typedef Predicate<T> = bool Function(T);
List<T> filter<T>(List<T> items, Predicate<T> predicate) {
return items.where(predicate).toList();
}
void main() {
print(filter([1, 2, 3, 4, 5], (number) => number > 2));
print(filter(['a', 'bb', 'ccc'], (word) => word.length > 1));
} Dart's
typedef creates a named alias for a function type. It improves readability in complex type signatures and enables documentation and reuse of function shapes. TypeScript has the same feature with type Predicate<T> = (value: T) => boolean.Classes & OOP
Class definition
class Person {
constructor(name, age) {
this.name = name;
this.age = age;
}
greet() {
console.log(`Hi, I'm ${this.name}`);
}
}
const person = new Person('Alice', 30);
person.greet(); class Person {
String name;
int age;
Person(this.name, this.age); // initializer shorthand
void greet() {
print("Hi, I'm $name");
}
}
void main() {
final person = Person('Alice', 30);
person.greet();
} Dart's
Person(this.name, this.age) is an initializer shorthand — it assigns constructor arguments to instance fields automatically. Dart does not require new to construct objects (it's optional and discouraged since Dart 2). Fields must be declared with their types in the class body.Named constructors
// JavaScript — static factory methods
class Color {
constructor(red, green, blue) {
this.red = red; this.green = green; this.blue = blue;
}
static black() { return new Color(0, 0, 0); }
static grey(level) { return new Color(level, level, level); }
}
console.log(Color.black());
console.log(Color.grey(128)); class Color {
final int red, green, blue;
Color(this.red, this.green, this.blue);
Color.black() : red = 0, green = 0, blue = 0;
Color.grey(int level) : red = level, green = level, blue = level;
@override
String toString() => 'Color($red, $green, $blue)';
}
void main() {
print(Color(255, 0, 0));
print(Color.black());
print(Color.grey(128));
} Dart classes can have multiple named constructors following the
ClassName.constructorName() pattern — a feature JavaScript lacks. The initializer list (after :) sets final fields before the constructor body runs. JavaScript approximates this with static factory methods.Getters and setters
class Temperature {
#celsius;
constructor(celsius) { this.#celsius = celsius; }
get fahrenheit() { return this.#celsius * 9 / 5 + 32; }
set fahrenheit(f) { this.#celsius = (f - 32) * 5 / 9; }
}
const temp = new Temperature(100);
console.log(temp.fahrenheit);
temp.fahrenheit = 32;
console.log(temp.fahrenheit); class Temperature {
double celsius;
Temperature(this.celsius);
double get fahrenheit => celsius * 9 / 5 + 32;
set fahrenheit(double value) => celsius = (value - 32) * 5 / 9;
}
void main() {
final temp = Temperature(100);
print(temp.fahrenheit); // 212.0
temp.fahrenheit = 32;
print(temp.fahrenheit); // 32.0
} Dart getters and setters use the
get and set keywords, similar to JavaScript. Getters are accessed like properties (temp.fahrenheit), not method calls. Dart does not have JavaScript's private field syntax (#field) — use a leading underscore (_field) for library-private access.Inheritance
class Animal {
constructor(name) { this.name = name; }
speak() { console.log(`${this.name} makes a sound.`); }
}
class Dog extends Animal {
speak() { console.log(`${this.name} barks.`); }
}
const dog = new Dog('Rex');
dog.speak();
console.log(dog instanceof Animal); class Animal {
final String name;
Animal(this.name);
void speak() => print('$name makes a sound.');
}
class Dog extends Animal {
Dog(super.name); // super parameter passes arg to parent
@override
void speak() => print('$name barks.');
}
void main() {
final dog = Dog('Rex');
dog.speak();
print(dog is Animal); // true
} Dart uses
extends for inheritance, like JavaScript. The super.name syntax in the constructor (Dart 2.17+) passes parameters directly to the superclass constructor without repetition. The is operator checks runtime types — equivalent to JavaScript's instanceof.Abstract classes
// JavaScript — simulate with runtime errors
class Shape {
area() { throw new Error('area() must be implemented'); }
}
class Circle extends Shape {
constructor(radius) { super(); this.radius = radius; }
area() { return Math.PI * this.radius ** 2; }
}
const circle = new Circle(5);
console.log(circle.area().toFixed(2)); import 'dart:math';
abstract class Shape {
double area(); // abstract method — no body
}
class Circle extends Shape {
final double radius;
Circle(this.radius);
@override
double area() => pi * radius * radius;
}
void main() {
final circle = Circle(5);
print(circle.area().toStringAsFixed(2));
// Shape(); // compile error — cannot instantiate abstract class
} Dart's
abstract keyword prevents direct instantiation and marks methods without bodies as abstract — subclasses must implement them. This is a compile-time guarantee, unlike JavaScript's runtime-error pattern. Dart 3 also introduced sealed class and interface class for richer hierarchies.Async / Futures
Future — Dart's Promise
async function fetchGreeting(name) {
const greeting = await Promise.resolve(`Hello, ${name}!`);
return greeting;
}
fetchGreeting('Alice').then(console.log); Future<String> fetchGreeting(String name) async {
final greeting = await Future.value('Hello, $name!');
return greeting;
}
Future<void> main() async {
final result = await fetchGreeting('Alice');
print(result);
} Dart's
Future<T> is equivalent to JavaScript's Promise<T>. The async/await syntax is nearly identical. Key difference: Dart functions must declare their return type as Future<T> when they are async. A main() function can be async..then() chaining
Promise.resolve(42)
.then(value => value * 2)
.then(value => `Result: ${value}`)
.then(console.log); Future<void> main() async {
final result = await Future.value(42)
.then((value) => value * 2)
.then((value) => 'Result: $value');
print(result);
} Dart futures support
.then() chaining exactly like JavaScript promises. The callback passed to .then() can return a plain value or another Future. In practice, async/await is preferred over chaining for readability.Stream — async sequence of values
// JavaScript — async generator
async function* countdown(from) {
for (let i = from; i >= 1; i--) {
yield i;
}
}
(async () => {
for await (const number of countdown(3)) {
console.log(number);
}
})(); Stream<int> countdown(int from) async* {
for (int i = from; i >= 1; i--) {
yield i;
}
}
Future<void> main() async {
await for (final number in countdown(3)) {
print(number);
}
} Dart's
Stream<T> is the async counterpart of JavaScript's async iterables / generators. An async* function can yield values one at a time. Use await for to iterate. Dart streams are either single-subscription (default) or broadcast.Async error handling
async function riskyOperation() {
throw new Error('something went wrong');
}
async function main() {
try {
await riskyOperation();
} catch (error) {
console.log('Caught:', error.message);
}
}
main(); Future<void> riskyOperation() async {
throw Exception('something went wrong');
}
Future<void> main() async {
try {
await riskyOperation();
} catch (error) {
print('Caught: $error');
}
} Async error handling in Dart uses
try/catch inside async functions, just like JavaScript. Dart also has .catchError() on futures for chain-style handling. Within catch, use on ExceptionType to catch specific exception types.Error Handling
try / catch / finally
try {
JSON.parse('not json');
} catch (error) {
console.log('Parse error:', error.message);
} finally {
console.log('always runs');
} import 'dart:convert';
void main() {
try {
final result = jsonDecode('not json');
print(result);
} catch (error) {
print('Parse error: $error');
} finally {
print('always runs');
}
} Dart's
try/catch/finally works identically to JavaScript's. Dart also adds on ExceptionType before catch to filter by exception type. The finally block always runs, even if an exception is thrown or rethrown.Catching specific exception types
function riskyParse(text) {
const number = Number(text);
if (isNaN(number)) throw new TypeError('Not a number: ' + text);
return number;
}
try {
riskyParse('abc');
} catch (error) {
if (error instanceof TypeError) {
console.log('Type error:', error.message);
} else { throw error; }
} void riskyParse(String text) {
final number = int.tryParse(text);
if (number == null) throw FormatException('Not a number: $text');
print(number);
}
void main() {
try {
riskyParse('abc');
} on FormatException catch (error) {
print('Format error: ${error.message}');
} catch (error) {
print('Unknown error: $error');
rethrow;
}
} Dart's
on ExceptionType catch (e) catches only that specific type — much cleaner than JavaScript's if (e instanceof Type) inside a catch block. Chain multiple on clauses for different types. Use rethrow to re-raise the current exception.Custom exception classes
class ValidationError extends Error {
constructor(field, message) {
super(message);
this.field = field;
this.name = 'ValidationError';
}
}
try {
throw new ValidationError('email', 'Invalid email format');
} catch (error) {
console.log(`${error.field}: ${error.message}`);
} class ValidationException implements Exception {
final String field;
final String message;
ValidationException(this.field, this.message);
@override
String toString() => 'ValidationException: [$field] $message';
}
void main() {
try {
throw ValidationException('email', 'Invalid email format');
} on ValidationException catch (error) {
print('${error.field}: ${error.message}');
}
} Dart custom exceptions implement the
Exception interface (or Error for programming mistakes). Unlike JavaScript where exceptions can be any value, Dart convention distinguishes between Exception (recoverable) and Error (programming mistakes like index out of bounds).Pattern Matching
Type patterns in switch
function describe(value) {
if (typeof value === 'number') return `int: ${value}`;
if (typeof value === 'string') return `String: "${value}"`;
if (Array.isArray(value)) return `Array with ${value.length} items`;
return 'unknown';
}
console.log(describe(42));
console.log(describe('hello'));
console.log(describe([1, 2, 3])); String describe(Object value) {
return switch (value) {
int number => 'int: $number',
String text => 'String: "$text"',
List list => 'List with ${list.length} items',
_ => 'unknown',
};
}
void main() {
print(describe(42));
print(describe('hello'));
print(describe([1, 2, 3]));
} Dart 3's switch expressions support type patterns — the compiler checks the type and promotes the variable to that type within the arm. JavaScript has no built-in pattern matching; you must manually check
typeof or instanceof.Records — lightweight value tuples
// JavaScript — use objects for multiple returns
function minMax(numbers) {
return { min: Math.min(...numbers), max: Math.max(...numbers) };
}
const { min, max } = minMax([3, 1, 4, 1, 5, 9]);
console.log(min, max); (int, int) minMax(List<int> numbers) {
final sorted = [...numbers]..sort();
return (sorted.first, sorted.last);
}
void main() {
final (minimum, maximum) = minMax([3, 1, 4, 1, 5, 9]);
print('$minimum $maximum');
// Named record fields
final point = (x: 3.0, y: 4.0);
print(point.x);
print(point.y);
} Dart 3 introduced records — immutable, anonymous value types that bundle multiple values. They're typed, destructurable, and have value equality (two records with the same fields and values are equal). JavaScript objects serve a similar purpose but are reference types with no value equality.
Destructuring patterns
const [first, second, ...rest] = [1, 2, 3, 4, 5];
console.log(first);
console.log(second);
console.log(rest); void main() {
// List pattern binding
final numbers = [1, 2, 3, 4, 5];
final [first, second, ...rest] = numbers;
print(first);
print(second);
print(rest);
// Record destructuring
final point = (x: 10, y: 20);
final (:x, :y) = point;
print('$x, $y');
} Dart 3 supports destructuring patterns for lists (
[first, ...rest]), records ((:x, :y)), and maps. The :name shorthand in record patterns binds the field to a variable with the same name, mirroring JavaScript's object destructuring shorthand.Guarded patterns with when
function classify(value) {
if (typeof value === 'string' && value.length > 5) return 'long string';
if (typeof value === 'number' && value > 0) return 'positive number';
return 'other';
}
console.log(classify('hello world'));
console.log(classify(42)); String classify(Object value) {
return switch (value) {
String text when text.length > 5 => 'long string',
int number when number > 0 => 'positive number',
_ => 'other',
};
}
void main() {
print(classify('hello world'));
print(classify(42));
print(classify(-5));
} The
when guard clause adds an extra condition to a pattern. Even if the pattern matches the type, the arm is skipped if the guard is false. This combines type checking and value checking in a single, readable expression.Extension Methods
Extension methods
// JavaScript — add to prototype (global, discouraged)
String.prototype.isPalindrome = function() {
const cleaned = this.toLowerCase().replace(/[^a-z]/g, '');
return cleaned === cleaned.split('').reverse().join('');
};
console.log('racecar'.isPalindrome());
console.log('hello'.isPalindrome()); extension StringExtensions on String {
bool get isPalindrome {
final cleaned = toLowerCase().replaceAll(RegExp(r'[^a-z]'), '');
return cleaned == cleaned.split('').reversed.join();
}
}
void main() {
print('racecar'.isPalindrome);
print('hello'.isPalindrome);
} Dart extension methods add methods and getters to existing types without subclassing or modifying the original class. Unlike JavaScript's prototype mutation, Dart extensions are scoped — they only apply when the extension's library is imported, avoiding global pollution.
Extensions on generic types
// JavaScript — add to Array prototype
Array.prototype.second = function() { return this[1]; };
console.log([10, 20, 30].second());
console.log([].second()); extension IterableExtensions<T> on Iterable<T> {
T? get second => length >= 2 ? elementAt(1) : null;
}
void main() {
final numbers = [10, 20, 30, 40];
print(numbers.second); // 20
print(<int>[].second); // null
print({'a', 'b', 'c'}.second); // works on Set too
} Extensions can be generic — an extension
on Iterable<T> adds methods to every iterable of any element type, including List, Set, and custom iterables. The extension is resolved at compile time and adds zero runtime overhead.Mixins
Mixin declaration and use
// JavaScript — mixin pattern via Object.assign
const Serializable = {
serialize() { return JSON.stringify(this); },
};
class User {
constructor(name) { this.name = name; }
}
Object.assign(User.prototype, Serializable);
const user = new User('Alice');
console.log(user.serialize()); mixin Serializable {
Map<String, dynamic> toJson(); // abstract — class must implement
String serialize() => toJson().toString();
}
class User with Serializable {
final String name;
final int age;
User(this.name, this.age);
@override
Map<String, dynamic> toJson() => {'name': name, 'age': age};
}
void main() {
final user = User('Alice', 30);
print(user.serialize());
} Dart's
mixin keyword declares a reusable unit of functionality. Classes use with to apply mixins. A mixin can declare abstract methods that the applying class must implement. Unlike JavaScript's Object.assign pattern, Dart mixins are type-checked at compile time.Mixin with on constraint
// JavaScript — no type-constrained mixins
// Any object gets the mixin, even if methods are missing
const LoggableMixin = {
log(message) {
console.log(`[${this.constructor.name}] ${message}`);
}
}; class Service {
String get serviceName => runtimeType.toString();
}
mixin Logger on Service {
void log(String message) {
print('[$serviceName] $message'); // safe: Service guarantees this
}
}
class UserService extends Service with Logger {
void createUser(String name) {
log('Creating user: $name');
}
}
void main() {
UserService().createUser('Alice');
} The
on ClassName constraint restricts which classes can use a mixin — only subclasses of Service can apply Logger. This lets the mixin safely call methods defined on Service. JavaScript mixins have no such restriction and may fail at runtime if expected methods are missing.Multiple mixins
// JavaScript — apply multiple mixin objects
const Timestamped = { createdAt: new Date().getFullYear() };
const Tagged = {
addTag(tag) { (this.tags = this.tags || []).push(tag); }
};
class Article {}
Object.assign(Article.prototype, Timestamped, Tagged);
const article = new Article();
article.addTag('dart');
console.log(article.tags); mixin Timestamped {
DateTime get createdAt => DateTime(2026, 1, 1);
}
mixin Tagged {
final List<String> tags = [];
void addTag(String tag) => tags.add(tag);
}
class Article with Timestamped, Tagged {
final String title;
Article(this.title);
}
void main() {
final article = Article('Dart Mixins');
article.addTag('dart');
article.addTag('flutter');
print(article.tags);
print(article.createdAt.year);
} Dart classes can use multiple mixins separated by commas:
class Foo with MixinA, MixinB. Mixins are applied in order (linearized left-to-right) — if two mixins define the same method, the last one wins. This linearization is deterministic and avoids diamond-inheritance ambiguity.