PONY λ M2 Modula-2

JavaScript.CodeCompared.To/Dart

An interactive executable cheatsheet comparing JavaScript and Dart

JavaScript (ES2025) Dart 3.7
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.