Hello World & Basics
Hello, World
print("Hello, World!") void main() {
print("Hello, World!");
} Dart has no top-level statement mode: execution begins at a required
void main(). Swift lets you write statements at file scope; Dart insists on the entry-point function, and every statement ends in a semicolon.Comments
// a line comment
/* a block comment */
/// A doc comment for the next declaration.
let answer = 42
print(answer) void main() {
// a line comment
/* a block comment */
/// A doc comment for the next declaration.
final answer = 42;
print(answer);
} Comment syntax is identical, including the triple-slash
/// documentation comment. Dart’s doc comments feed dart doc the way Swift’s feed DocC.Printing values
let name = "Ada"
let count = 3
print("\(name) has \(count) items") void main() {
final name = "Ada";
final count = 3;
print("$name has $count items");
} Both use
print, but the interpolation sigil differs: Swift’s \(expr) becomes Dart’s $name for a bare identifier and ${expr} for anything more complex.Variables & Constants
let/var → final/var
let fixed = 10 // immutable
var counter = 0 // mutable
counter += 1
print("\(fixed) \(counter)") void main() {
final fixed = 10; // immutable binding
var counter = 0; // mutable
counter += 1;
print("$fixed $counter");
} Swift’s
let maps to Dart’s final, and var is mutable in both. The keyword flips: in Swift the short word (let) is the immutable one, in Dart the short word (var) is the mutable one.Compile-time const
// Swift's 'let' is a runtime constant; there is no
// separate compile-time const keyword for values.
let maxUsers = 100
print(maxUsers) void main() {
const maxUsers = 100; // canonicalized at compile time
const list = [1, 2, 3]; // deeply immutable, shared instance
print(maxUsers);
print(list);
} Dart draws a distinction Swift does not:
final is set once at run time, while const must be known at compile time and is canonicalized — two identical const values are the same object. This underpins Dart’s const constructors.Type inference & annotations
let inferred = 3.14 // Double
let annotated: Int = 42
let items: [String] = ["a", "b"]
print("\(inferred) \(annotated) \(items)") void main() {
final inferred = 3.14; // double
final int annotated = 42;
final List<String> items = ["a", "b"];
print("$inferred $annotated $items");
} Both infer from the initializer. The annotation position differs: Swift writes
name: Type, Dart writes Type name — the type precedes the name, C-style.The dynamic escape hatch
// Swift's 'Any' still requires a cast before use;
// there is no unchecked dynamic dispatch.
let anything: Any = "hello"
if let text = anything as? String {
print(text.uppercased())
} void main() {
dynamic anything = "hello";
// No cast needed — the call is checked at run time.
print(anything.toUpperCase());
anything = 42;
print(anything + 1); // also fine at run time
} Dart’s
dynamic turns off static checking for that variable: any method call compiles and is resolved at run time, throwing NoSuchMethodError if absent. Swift’s Any has no such hole — you must cast with as? before sending a message.Optionals & Null Safety
Optional → nullable type
var middleName: String? = nil
middleName = "Quinn"
print(middleName ?? "(none)") void main() {
String? middleName = null;
middleName = "Quinn";
print(middleName ?? "(none)");
} Dart has sound null safety like Swift:
String? is the nullable type and ?? is the same coalescing operator. The difference is spelling — Swift’s Optional<T> is a real enum you can pattern-match, while Dart’s T? is a union of T and Null.if let → type promotion
let raw: String? = "42"
if let raw, let number = Int(raw) {
print("parsed \(number)")
} else {
print("no value")
} void main() {
String? raw = "42";
if (raw != null) {
// raw is promoted to non-nullable String here
final number = int.tryParse(raw);
print(number != null ? "parsed $number" : "bad");
} else {
print("no value");
}
} Dart has no
if let; instead a != null check promotes the variable to its non-nullable type for the rest of the branch, so you keep using the same name. Promotion works only on local variables, not on mutable fields.Optional chaining & force
let words: [String]? = ["hello", "world"]
print(words?.count ?? 0) // 2
let forced = words! // trap if nil
print(forced[0]) void main() {
List<String>? words = ["hello", "world"];
print(words?.length ?? 0); // 2
final forced = words!; // throws if null
print(forced[0]);
} The operators line up almost exactly:
?. for safe access and postfix ! to assert non-null. Swift calls ! "force unwrap"; Dart calls it the "null assertion operator", and both crash if the value is actually absent.late variables
// Swift's nearest match is the implicitly-unwrapped
// optional: declared without a value, asserted non-nil
// on every use.
var greeting: String! = nil
greeting = "computed once"
print(greeting!) late String greeting;
void main() {
// Assigned before first read; no null needed.
greeting = "computed once";
print(greeting);
} Dart’s
late promises the compiler a non-nullable variable will be assigned before it is read, deferring the check to run time — the nearest match to Swift’s implicitly-unwrapped optional (String!), but without leaving the variable nullable. Paired with an initializer it also gives lazy, compute-on-first-read initialization, the role Swift’s lazy var fills inside a type.Strings
Interpolation
let width = 3
let height = 4
print("area is \(width * height)") void main() {
final width = 3;
final height = 4;
print("area is ${width * height}");
} A bare variable interpolates as
$name, but any expression needs the braces: ${width * height}. Swift wraps every interpolation in \( ) regardless of complexity.Common operations
let phrase = "hello world"
print(phrase.count)
print(phrase.uppercased())
print(phrase.contains("world"))
print(phrase.split(separator: " ")) void main() {
final phrase = "hello world";
print(phrase.length);
print(phrase.toUpperCase());
print(phrase.contains("world"));
print(phrase.split(" "));
} The method names differ (
count → length, uppercased() → toUpperCase()), and Dart’s split takes a plain string or pattern rather than a Character separator. Note length counts UTF-16 code units, unlike Swift’s grapheme-cluster count.Multiline & raw strings
let poem = """
line one
line two
"""
let path = #"C:\temp\new"#
print(poem)
print(path) void main() {
final poem = '''
line one
line two''';
final path = r"C:\temp\new";
print(poem);
print(path);
} Dart’s multiline literal uses triple quotes (single or double), and its raw string prefix is a lowercase
r rather than Swift’s #"…"# delimiters. In a raw string, backslashes are literal in both languages.Numbers
Int/Double → int/double
let whole: Int = 7
let fraction: Double = 3.5
print(whole + Int(fraction)) // explicit conversion void main() {
int whole = 7;
double fraction = 3.5;
print(whole + fraction.toInt()); // explicit conversion
} Both keep
int and double distinct and refuse to mix them implicitly. Dart adds a shared supertype num that either can flow into, whereas Swift has no common numeric supertype — you convert explicitly with Int( ) / toInt().Integer division
let total = 7
let half = total / 2 // 3 — Int / Int stays Int
let exact = 7.0 / 2.0 // 3.5
print("\(half) \(exact)") void main() {
final total = 7;
final half = total ~/ 2; // 3 — truncating division
final ratio = total / 2; // 3.5 — / always yields double
print("$half $ratio");
} This is a genuine trap: in Dart
/ always produces a double, even for two ints, so 7 / 2 is 3.5. Truncating integer division has its own operator, ~/. In Swift, Int / Int stays an Int.Number methods
print(abs(-4))
print((10).isMultiple(of: 2))
print(Int(3.7.rounded()))
print(max(3, 8)) import 'dart:math' as math;
void main() {
print((-4).abs());
print(10.isEven);
print(3.7.round());
print(math.max(3, 8));
} Dart numbers carry many helpers as methods and getters:
abs() and round() are methods, and isEven/isOdd are properties rather than the method call Swift uses. But max is a free function in dart:math, not a method — unlike Swift’s global max(_:_:). Note (-4).abs() needs the parentheses so the minus binds to the literal.Collections
Array → List
var numbers = [1, 2, 3]
numbers.append(4)
print(numbers)
print(numbers.first ?? 0)
print(numbers.map { $0 * 2 }) void main() {
final numbers = [1, 2, 3];
numbers.add(4);
print(numbers);
print(numbers.first);
print(numbers.map((n) => n * 2).toList());
} Dart’s
List replaces Swift’s Array, but two things bite: List is a reference type (a final list is still add-able), and map returns a lazy Iterable that you must finish with .toList(). Swift’s map returns an Array directly.Dictionary → Map
var ages = ["Ada": 36, "Alan": 41]
ages["Grace"] = 45
print(ages["Ada"] ?? 0)
print(ages.count) void main() {
final ages = {"Ada": 36, "Alan": 41};
ages["Grace"] = 45;
print(ages["Ada"]); // null if absent
print(ages.length);
} The literal syntax is nearly identical. The catch: indexing a Dart
Map returns a nullable value (V?), so ages["Ada"] has type int? — the compiler forces you to handle the missing-key case, much as Swift’s dictionary subscript returns an Optional.Set
let unique: Set = [1, 2, 2, 3]
print(unique.contains(2))
print(unique.count) void main() {
final unique = {1, 2, 2, 3};
print(unique.contains(2));
print(unique.length);
} A brace literal with no colons is a
Set in Dart, just as {} with colons is a Map. Beware the empty case: {} is an empty Map, so an empty set needs an explicit <int>{} type argument.Spread & collection-if/for
let base = [1, 2, 3]
let includeZero = true
var built = [Int]()
if includeZero { built.append(0) }
built.append(contentsOf: base)
built += base.map { $0 * 10 }
print(built) void main() {
final base = [1, 2, 3];
const includeZero = true;
final built = [
if (includeZero) 0,
...base,
for (final n in base) n * 10,
];
print(built);
} Dart builds collections declaratively with the spread operator
..., the if-element, and the for-element right inside the literal. There is no Swift equivalent — you would append imperatively or chain map/filter. This shines in Flutter widget lists.Value vs Reference Semantics
No structs — everything is a reference
struct Point { var x: Int; var y: Int }
var first = Point(x: 1, y: 2)
var second = first // COPIES
second.x = 99
print("\(first.x) \(second.x)") // 1 99 class Point {
int x, y;
Point(this.x, this.y);
}
void main() {
final first = Point(1, 2);
final second = first; // SAME object, no copy
second.x = 99;
print("${first.x} ${second.x}"); // 99 99
} This is the single most important difference. Dart has no value types: a class instance is always a reference, so
second = first aliases the same object and the mutation is visible through both. The Swift struct copies. Reach for records or immutable classes when you want value semantics.Records → value semantics
// A Swift tuple is a value type with a fixed shape.
let point = (x: 1, y: 2)
print(point.x)
let (a, b) = (10, 20)
print("\(a) \(b)") void main() {
// A record: value type, structural equality, fixed shape.
final point = (x: 1, y: 2);
print(point.x);
final (a, b) = (10, 20); // positional fields $1, $2
print("$a $b");
} Dart records (3.0) are the value types the language otherwise lacks: they compare by structure, copy by value, and destructure like a Swift tuple. Named fields are accessed by name (
point.x); positional fields by $1, $2. They are the idiomatic way to return multiple values.Immutable "copyWith"
struct Config {
var host: String
var port: Int
}
let base = Config(host: "localhost", port: 80)
var updated = base // copy
updated.port = 443
print("\(base.port) \(updated.port)") class Config {
final String host;
final int port;
const Config({required this.host, required this.port});
Config copyWith({String? host, int? port}) =>
Config(host: host ?? this.host, port: port ?? this.port);
}
void main() {
const base = Config(host: "localhost", port: 80);
final updated = base.copyWith(port: 443);
print("${base.port} ${updated.port}");
} Because a class cannot copy itself, the community convention is a hand-written (or generated)
copyWith that returns a new instance with some fields replaced — exactly the mutate-a-copy that Swift gets for free from var updated = base. Flutter code is full of these.Control Flow
if, for, while
for index in 0..<3 {
print(index)
}
var count = 0
while count < 2 {
count += 1
}
print("done \(count)") void main() {
for (var index = 0; index < 3; index++) {
print(index);
}
var count = 0;
while (count < 2) {
count += 1;
}
print("done $count");
} Dart uses the C-style three-clause
for and requires parentheses around every condition. There is no Swift range operator; iterate with for (final x in iterable) or a counting loop. Note the braces are mandatory even for a single statement in idiomatic Dart.for-in over collections
let colors = ["red", "green", "blue"]
for (index, color) in colors.enumerated() {
print("\(index): \(color)")
} void main() {
final colors = ["red", "green", "blue"];
for (final (index, color) in colors.indexed) {
print("$index: $color");
}
} Dart’s
indexed getter pairs each element with its position as a record, which the for loop destructures — the direct counterpart to Swift’s enumerated(). Before records existed, Dart code used asMap().entries for this.Ternary & cascades
let score = 72
let grade = score >= 60 ? "pass" : "fail"
print(grade)
// Swift has no cascade; repeat the receiver.
var parts = [String]()
parts.append("a")
parts.append("b")
print(parts) void main() {
final score = 72;
final grade = score >= 60 ? "pass" : "fail";
print(grade);
// Cascade: call many methods on one receiver.
final parts = <String>[]
..add("a")
..add("b");
print(parts);
} The ternary is identical, but the cascade (
..) is pure Dart: it calls a sequence of methods on the same object and evaluates to that object, so you configure a value without naming it repeatedly. Swift has nothing like it.Functions & Closures
Function definition
func add(_ x: Int, _ y: Int) -> Int {
return x + y
}
func square(_ x: Int) -> Int { x * x } // implicit return
print(add(3, 4))
print(square(5)) int add(int x, int y) {
return x + y;
}
int square(int x) => x * x; // arrow body
void main() {
print(add(3, 4));
print(square(5));
} Dart writes the return type first and the parameter types before their names. The
=> arrow body is Dart’s shorthand for a function whose whole body is one expression — the analog of Swift’s implicit single-expression return.Argument labels → named parameters
func makeUser(name: String, admin: Bool = false) -> String {
return "\(name) admin=\(admin)"
}
print(makeUser(name: "Ada"))
print(makeUser(name: "Alan", admin: true)) String makeUser({required String name, bool admin = false}) {
return "$name admin=$admin";
}
void main() {
print(makeUser(name: "Ada"));
print(makeUser(name: "Alan", admin: true));
} Dart named parameters go inside
{ } and are optional by default — the opposite of Swift, where every argument label is required unless you write _. Mark a named parameter required to force it, and give a default to make it truly optional.Closures & trailing syntax
let numbers = [1, 2, 3, 4]
let evens = numbers.filter { $0 % 2 == 0 }
let doubled = numbers.map { value in value * 2 }
print(evens)
print(doubled) void main() {
final numbers = [1, 2, 3, 4];
final evens = numbers.where((n) => n % 2 == 0).toList();
final doubled = numbers.map((value) => value * 2).toList();
print(evens);
print(doubled);
} Dart closures always list their parameters explicitly —
(value) => … — with no $0 shorthand and no trailing-closure sugar, so the function goes inside the parentheses. Also note filter is named where, and it returns a lazy Iterable.Functions as values
func greet(_ name: String) -> String { "Hi, \(name)" }
let fn: (String) -> String = greet
print(fn("Ada"))
["a", "b"].forEach { print(fn($0)) } String greet(String name) => "Hi, $name";
void main() {
final String Function(String) fn = greet;
print(fn("Ada"));
["a", "b"].forEach((name) => print(fn(name)));
} A function type is written
ReturnType Function(ArgTypes) in Dart, versus Swift’s (ArgTypes) -> ReturnType. Otherwise functions are first-class in both — assignable, passable, and returnable.Classes & Constructors
Constructors
class Animal {
let name: String
var legs: Int
init(name: String, legs: Int = 4) {
self.name = name
self.legs = legs
}
}
let dog = Animal(name: "Rex")
print("\(dog.name) \(dog.legs)") class Animal {
final String name;
int legs;
Animal(this.name, {this.legs = 4}); // this.name is sugar
}
void main() {
final dog = Animal("Rex");
print("${dog.name} ${dog.legs}");
} Dart’s
this.name parameter assigns the field directly, collapsing Swift’s init boilerplate. The constructor is named after the class, and there is no self/init keyword pair — just ClassName(...).Named constructors
struct Point {
var x: Double; var y: Double
init(x: Double, y: Double) { self.x = x; self.y = y }
// Swift uses static factory methods for alternatives.
static func origin() -> Point { Point(x: 0, y: 0) }
}
let p = Point.origin()
print("\(p.x) \(p.y)") class Point {
final double x, y;
Point(this.x, this.y);
Point.origin() : x = 0, y = 0; // named constructor
}
void main() {
final p = Point.origin();
print("${p.x} ${p.y}");
} Dart supports multiple named constructors (
Point.origin()) as a first-class feature, using an initializer list after the colon to set final fields. Swift achieves the same with additional inits or static factory methods.Computed properties
struct Circle {
var radius: Double
var area: Double { radius * radius * 3.14159 }
}
let circle = Circle(radius: 2)
print(circle.area) class Circle {
double radius;
Circle(this.radius);
double get area => radius * radius * 3.14159;
}
void main() {
final circle = Circle(2);
print(circle.area);
} Dart spells a computed property as a
get (and optionally set) member, where Swift uses a braces block with an implicit getter. Both are accessed like a stored field, with no call parentheses.Inheritance & super
class Vehicle {
func describe() -> String { "a vehicle" }
}
class Car: Vehicle {
override func describe() -> String {
"a car, which is " + super.describe()
}
}
print(Car().describe()) class Vehicle {
String describe() => "a vehicle";
}
class Car extends Vehicle {
@override
String describe() => "a car, which is ${super.describe()}";
}
void main() {
print(Car().describe());
} Dart uses
extends (not :) for the superclass and an @override annotation that is advisory rather than required — unlike Swift’s mandatory override keyword. super works the same in both.Enums & Sealed Classes
Simple enums
enum Direction: String {
case north = "N", south = "S", east = "E", west = "W"
}
let heading = Direction.north
print(heading.rawValue) enum Direction {
north("N"), south("S"), east("E"), west("W");
final String code;
const Direction(this.code);
}
void main() {
final heading = Direction.north;
print(heading.code);
} Dart’s enhanced enums (2.17+) carry fields and a const constructor, which is how you attach a payload like Swift’s raw value — but there is no built-in
rawValue, so you declare the field (code) yourself. Each case calls the constructor.Enums with methods
enum Planet {
case earth, mars
var gravity: Double {
switch self {
case .earth: return 9.8
case .mars: return 3.7
}
}
}
print(Planet.mars.gravity) enum Planet {
earth,
mars;
double get gravity => switch (this) {
Planet.earth => 9.8,
Planet.mars => 3.7,
};
}
void main() {
print(Planet.mars.gravity);
} Both languages let an enum declare methods and computed properties. Dart separates the case list from members with a semicolon, and a
switch over an enum is exhaustive in both — the compiler flags a missing case.Associated values → sealed classes
enum Shape {
case circle(radius: Double)
case rectangle(width: Double, height: Double)
}
func area(_ shape: Shape) -> Double {
switch shape {
case .circle(let radius): return radius * radius * 3.14159
case .rectangle(let width, let height): return width * height
}
}
print(area(.circle(radius: 2))) sealed class Shape {}
class Circle extends Shape {
final double radius;
Circle(this.radius);
}
class Rectangle extends Shape {
final double width, height;
Rectangle(this.width, this.height);
}
double area(Shape shape) => switch (shape) {
Circle(:final radius) => radius * radius * 3.14159,
Rectangle(:final width, :final height) => width * height,
};
void main() {
print(area(Circle(2)));
} This is the enum false friend. A Swift enum with associated values is not a Dart
enum — Dart enum cases cannot carry per-case data. Model it as a sealed class hierarchy; the switch over the sealed type is still exhaustive, and object patterns (Circle(:final radius)) bind the fields.Pattern Matching
switch expressions
func describe(_ n: Int) -> String {
switch n {
case 0: return "zero"
case 1...9: return "small"
default: return "big"
}
}
print(describe(5)) String describe(int n) => switch (n) {
0 => "zero",
>= 1 && <= 9 => "small",
_ => "big",
};
void main() {
print(describe(5));
} Dart 3.0’s
switch expression returns a value with => arms and uses _ as the wildcard. Relational and logical patterns (>= 1 && <= 9) replace Swift’s range case 1...9. Both are exhaustive and both are expressions.Destructuring in switch
let point = (2, 0)
switch point {
case (0, 0): print("origin")
case (let x, 0): print("on x-axis at \(x)")
case (0, let y): print("on y-axis at \(y)")
case (let x, let y): print("at \(x), \(y)")
} void main() {
final point = (2, 0);
switch (point) {
case (0, 0):
print("origin");
case (final x, 0):
print("on x-axis at $x");
case (0, final y):
print("on y-axis at $y");
case (final x, final y):
print("at $x, $y");
}
} Record patterns destructure positionally just like Swift tuple patterns, binding with
final x where Swift writes let x. A constant in a position (0) constrains the match. This is the feature that makes Dart 3 feel closest to Swift.if-case binding
let response: (Int, String) = (200, "OK")
if case (200, let message) = response {
print("success: \(message)")
} void main() {
final (int, String) response = (200, "OK");
if (response case (200, final message)) {
print("success: $message");
}
} Dart’s
if (value case Pattern) is the direct analog of Swift’s if case Pattern = value — a single-branch match that binds variables into the if body. The keyword order flips, but the behavior is the same.Protocols → Interfaces & Mixins
Protocol → implicit interface
protocol Greeter {
func greet() -> String
}
struct English: Greeter {
func greet() -> String { "Hello" }
}
func announce(_ g: Greeter) { print(g.greet()) }
announce(English()) // Every class defines an implicit interface.
abstract interface class Greeter {
String greet();
}
class English implements Greeter {
@override
String greet() => "Hello";
}
void announce(Greeter g) => print(g.greet());
void main() {
announce(English());
} Dart has no
protocol keyword; instead every class defines an implicit interface, and you use implements to conform to one. An abstract interface class declares a contract with no implementation — the closest match to a Swift protocol.Protocol extensions → mixins
protocol Logger {}
extension Logger {
func log(_ message: String) { print("[log] \(message)") }
}
struct Service: Logger {}
Service().log("started") mixin Logger {
void log(String message) => print("[log] $message");
}
class Service with Logger {}
void main() {
Service().log("started");
} A Dart
mixin supplies shared implementation to any class that uses with — the role Swift fills with a protocol extension that provides default methods. Unlike a protocol extension, a mixin can also declare state and can be constrained with on to require a superclass.Extensions
extension Int {
var squared: Int { self * self }
}
print(5.squared) extension IntExtras on int {
int get squared => this * this;
}
void main() {
print(5.squared);
} Both languages add members to existing types without subclassing. Dart’s extension is named (
IntExtras) and declares its receiver with on int, referring to the value as this rather than Swift’s self. The name lets you hide or resolve conflicting extensions.Generics
Generic functions
func firstOrNil<Element>(_ items: [Element]) -> Element? {
return items.first
}
print(firstOrNil([10, 20, 30]) ?? -1) T? firstOrNull<T>(List<T> items) {
return items.isEmpty ? null : items.first;
}
void main() {
print(firstOrNull([10, 20, 30]) ?? -1);
} Generic syntax matches closely: a type parameter in angle brackets, used as a normal type. The declaration order differs because Dart puts the return type first (
T? firstOrNull<T>(...)).Bounded type parameters
func largest<T: Comparable>(_ items: [T]) -> T {
return items.max()!
}
print(largest([3, 9, 2])) T largest<T extends Comparable<T>>(List<T> items) {
var result = items.first;
for (final item in items.skip(1)) {
if (item.compareTo(result) > 0) result = item;
}
return result;
}
void main() {
print(largest([3, 9, 2]));
} Dart bounds a type parameter with
extends where Swift uses :. A subtle difference from Swift generics: Dart generics are reified — the type argument survives to run time, so items is List<int> actually works, unlike on the JVM.Generic classes
struct Box<Value> {
let value: Value
func map<U>(_ transform: (Value) -> U) -> Box<U> {
Box<U>(value: transform(value))
}
}
let boxed = Box(value: 21).map { $0 * 2 }
print(boxed.value) class Box<T> {
final T value;
const Box(this.value);
Box<U> map<U>(U Function(T) transform) => Box(transform(value));
}
void main() {
final boxed = Box(21).map((n) => n * 2);
print(boxed.value);
} Generic classes work the same way in both languages. Note Dart infers the type argument of
Box(21) from the constructor argument, just as Swift infers Box(value: 21) — no explicit <int> needed.Error Handling
throws → throw/catch
enum ParseError: Error { case empty }
func parse(_ text: String) throws -> Int {
if text.isEmpty { throw ParseError.empty }
return Int(text) ?? 0
}
do {
print(try parse(""))
} catch {
print("failed: \(error)")
} class ParseError implements Exception {
final String message;
ParseError(this.message);
@override
String toString() => "ParseError: $message";
}
int parse(String text) {
if (text.isEmpty) throw ParseError("empty");
return int.tryParse(text) ?? 0;
}
void main() {
try {
print(parse(""));
} catch (error) {
print("failed: $error");
}
} The big shift: Dart exceptions are unchecked. A method that throws carries no
throws in its signature and callers are never forced to handle it — nothing like Swift’s try at every call site. You can throw any object, though implementing Exception is the convention.Typed catch & finally
enum NetError: Error { case timeout }
func fetch() throws { throw NetError.timeout }
do {
try fetch()
} catch NetError.timeout {
print("timed out")
} catch {
print("other: \(error)")
} class TimeoutError implements Exception {}
void fetch() => throw TimeoutError();
void main() {
try {
fetch();
} on TimeoutError {
print("timed out");
} catch (error) {
print("other: $error");
} finally {
print("cleaned up");
}
} Dart selects a handler by type with
on TypeName, and a bare catch (error) takes anything — versus Swift’s catch Pattern. Dart’s finally is the counterpart to Swift’s defer, though defer is scoped to the enclosing function rather than a try block.Concurrency
async/await
func doubled(_ x: Int) async -> Int {
return x * 2
}
let result = await doubled(21)
print(result) Future<int> doubled(int x) async {
return x * 2;
}
void main() async {
final result = await doubled(21);
print(result);
} An
async Dart function returns a Future<T> — the concrete type Swift keeps implicit behind async. You await a Future just as in Swift, and main itself can be async.Awaiting multiple futures
func value(_ n: Int) async -> Int { n }
async let a = value(1)
async let b = value(2)
let total = await a + b
print(total) Future<int> value(int n) async => n;
void main() async {
final results = await Future.wait([value(1), value(2)]);
print(results.reduce((a, b) => a + b));
} Where Swift kicks off concurrent work with
async let, Dart collects a list of Futures and awaits them together with Future.wait, which resolves to a list of results. Both let independent async work overlap rather than run in series.AsyncSequence → Stream
func countUp() -> AsyncStream<Int> {
AsyncStream { continuation in
for index in 1...3 { continuation.yield(index) }
continuation.finish()
}
}
for await value in countUp() {
print(value)
} Stream<int> countUp() async* {
for (var index = 1; index <= 3; index++) {
yield index;
}
}
void main() async {
await for (final value in countUp()) {
print(value);
}
} Dart’s
Stream is the analog of Swift’s AsyncSequence, and an async* generator with yield is far terser to write than building an AsyncStream by hand. You consume it with await for, mirroring Swift’s for await.Actors → isolates
// A Swift actor protects mutable state with the type
// system; other tasks await access to it.
actor Counter {
private var count = 0
func increment() { count += 1 }
func value() -> Int { count }
}
let counter = Counter()
await counter.increment()
await counter.increment()
print(await counter.value()) import 'dart:isolate';
int heavySum(int n) {
var total = 0;
for (var i = 1; i <= n; i++) total += i;
return total;
}
void main() async {
// An isolate has NO shared memory — data is copied across.
final result = await Isolate.run(() => heavySum(1000));
print(result);
} Dart’s concurrency model is fundamentally different: there is no shared mutable memory. An
Isolate has its own heap, and you communicate by copying messages, so data races are impossible by construction — a stronger guarantee than Swift’s actor, which shares memory but serializes access. Most Dart code stays on one isolate’s event loop and only spawns one for heavy CPU work.