Core Syntax #

Dart was designed with a clear philosophy: the syntax should feel familiar to developers who already know C#, Java, or JavaScript, but with improvements that remove the common pitfalls of those languages. This article isn’t just a list of features — every syntax construct is explained in the context of the problem it solves, complete with correct patterns and anti-patterns to avoid. The goal is to give you a solid mental map before you dive into the deeper topics in the following articles.

Anatomy of a Dart Program #

Every runnable Dart program has one entry point: the main() function. Understanding its basic structure will make all the code you write afterward feel coherent.

// File: hello.dart

// Import a standard library (optional, only if needed)
import 'dart:math';

// Main function — required entry point
void main() {
  var pesan = 'Hello, Dart!';
  print(pesan);

  // Dart is a strongly typed language
  int angka = 42;
  double phi = 3.14159;
  bool aktif = true;

  print('Square root of $angka: ${sqrt(angka.toDouble())}');
}

A few things stand out immediately from this example: types are written before the variable name, string interpolation uses $ and ${}, and every statement ends with a semicolon. These are the three most fundamental syntax rules in Dart.

flowchart TD
    A[.dart file] --> B[import statements]
    B --> C[Top-level declarations: variables, functions, classes]
    C --> D[void main]
    D --> E[First statement]
    E --> F[...]
    F --> G[Program ends]

Comments #

Comments aren’t just passive notes — they’re part of code communication. Dart supports three kinds of comments with different purposes, and choosing the wrong one can make tooling like dart doc miss your important documentation.

// Single-line comment — for short explanations inside functions

/*
  Multi-line comment — for longer explanations
  that need more than one line.
  Rarely used inside functions; more often
  for temporarily disabling blocks of code.
*/

/// Documentation comment — this is what dart doc processes
/// to generate automatic HTML documentation.
///
/// Use this format to document:
/// - Public functions and methods
/// - Classes and their properties
/// - Typedefs and extensions
///
/// [parameter] is a parameter name that will be linked automatically.
/// Returns a [String] containing the formatted message.
String formatPesan(String parameter) {
  return 'Message: $parameter';
}
// ANTI-PATTERN: a comment that only repeats the code
int umur = 25; // set umur to 25

// CORRECT: a comment that explains WHY, not WHAT
int umur = 25; // minimum age to register as a speaker
Run dart doc in your project directory to generate HTML documentation from /// comments. The results will be saved in the doc/api/ folder and can be opened directly in the browser.

Variables and Data Types #

Dart is a sound null safety language — meaning the compiler can guarantee that non-nullable variables will never be null at runtime. This eliminates an entire class of NullPointerException bugs common in other languages.

How to Declare Variables #

There are four main ways to declare variables in Dart, each with different trade-offs:

// 1. var — type is inferred, value can change
var nama = 'Budi';       // inferred: String
var umur = 25;           // inferred: int
nama = 'Siti';           // ✓ can be reassigned

// 2. Explicit type — more verbose but clearer
String kota = 'Jakarta';
int populasi = 10_000_000; // underscore as a thousands separator, valid in Dart

// 3. final — type inferred, value can only be set once
final tanggalLahir = DateTime(1998, 5, 20);
// tanggalLahir = DateTime(2000, 1, 1); // ✗ error: cannot be reassigned

// 4. const — value must be known at compile time
const pi = 3.14159265358979;
const appName = 'MyApp';
// const waktuSekarang = DateTime.now(); // ✗ error: DateTime.now() is not a compile-time constant
// ANTI-PATTERN: using var for every variable without thought
var x = hitungTotal();   // what type does hitungTotal() return? unclear
var y = x * 1.1;         // now what type is y?

// CORRECT: use explicit types when the type isn't obvious from the initial value
double subtotal = hitungTotal();
double totalDenganPajak = subtotal * 1.1;

Null Safety #

Null safety is one of the most important features of modern Dart. By default, all variables cannot be null — you must explicitly mark variables that can be null with the ? sign.

// Non-nullable variable — the compiler guarantees it's never null
String nama = 'Budi';
// nama = null; // ✗ compile error

// Nullable variable — must be handled before use
String? namaOptional;
namaOptional = null; // ✓ valid

// Null-aware operators to handle nullables safely
String tampilan = namaOptional ?? 'Guest';          // fallback if null
int? panjang = namaOptional?.length;               // safe access, result is int?
namaOptional ??= 'Default';                        // assign only if null

// Late initialization — non-nullable but initialized later
late String koneksiDatabase;
// koneksiDatabase is filled before first access
// ANTI-PATTERN: using nullable where non-nullable is enough
String? kota = 'Jakarta'; // why nullable? kota definitely has a value

// CORRECT: nullable only for data that may genuinely be absent
String kota = 'Jakarta';
String? kotaKelahiran; // the user may not have filled this in

Basic Data Types #

TypeDescriptionLiteral example
intWhole numbers, unbounded size42, -7, 0xFF
double64-bit decimal numbers3.14, -0.5, 1.0e10
numSupertype of int and doublecan hold both
StringUnicode text, immutable'hello', "world"
boolTrue/false valuetrue, false
List<T>Ordered array[1, 2, 3]
Set<T>Unordered unique collection{1, 2, 3}
Map<K,V>Key-value pairs{'a': 1}
dynamicAny type, not checked by the compileravoid if possible
ObjectSupertype of all non-nullable types

Operators #

Dart inherits most operators from the C family, but adds several very useful null-aware operators. Understanding these operator groups will make your code much more concise without losing clarity.

Arithmetic and Comparison Operators #

int a = 17;
int b = 5;

// Standard arithmetic
print(a + b);   // 22
print(a - b);   // 12
print(a * b);   // 85
print(a / b);   // 3.4 — always double
print(a ~/ b);  // 3   — integer division (floor division)
print(a % b);   // 2   — modulo/remainder

// Comparison — always returns bool
print(a == b);  // false
print(a != b);  // true
print(a > b);   // true
print(a >= b);  // true
// ANTI-PATTERN: using / for integer division
int total = 100;
int bagian = total / 3; // ✗ error: double can't be assigned to int

// CORRECT: use ~/ for an integer result
int bagianBenar = total ~/ 3; // ✓ result is 33

Null-Aware Operators #

This is one of the Dart syntax features that most sets it apart from other languages:

String? input = dapatkanInput(); // may be null

// ?? — use the right value if the left is null
String teks = input ?? 'default value';

// ??= — assign a value if the variable is still null
input ??= 'default';

// ?. — call a method/access a property only if not null
int? panjang = input?.length;

// !. — force non-null (use carefully, can throw if null)
int panjangPasti = input!.length; // make sure input is not null before this
// ANTI-PATTERN: verbose manual null checks
String hasil;
if (input != null) {
  hasil = input;
} else {
  hasil = 'default';
}

// CORRECT: use the ?? operator
String hasil = input ?? 'default';

Cascade Operator (..) #

The cascade operator lets you chain multiple method calls on the same object without repeating the variable name:

// ANTI-PATTERN: repeating the variable name for every operation
var buffer = StringBuffer();
buffer.write('Hello');
buffer.write(', ');
buffer.write('Dart');
buffer.writeln('!');

// CORRECT: use the cascade operator
var buffer = StringBuffer()
  ..write('Hello')
  ..write(', ')
  ..write('Dart')
  ..writeln('!');

print(buffer.toString()); // Hello, Dart!

Control Flow #

Dart has all the control flow constructs you’d expect, plus a few additions that make it more expressive.

Branching: if and switch #

int nilai = 85;

// Classic if-else
if (nilai >= 90) {
  print('A');
} else if (nilai >= 80) {
  print('B');
} else if (nilai >= 70) {
  print('C');
} else {
  print('Below standard');
}

// Conditional expression — for simple expressions
String grade = nilai >= 90 ? 'Graduated with honors' : 'Passed';

Dart 3 introduced switch expressions, which are far more powerful than the classic switch statement:

// Classic switch statement (old Dart)
String deskripsi;
switch (nilai) {
  case >= 90:
    deskripsi = 'Excellent';
    break;
  case >= 80:
    deskripsi = 'Good';
    break;
  default:
    deskripsi = 'Fair';
}

// switch expression (Dart 3+) — more concise and must be exhaustive
String deskripsi = switch (nilai) {
  >= 90 => 'Excellent',
  >= 80 => 'Good',
  >= 70 => 'Fair',
  _     => 'Below standard',  // _ is the wildcard/default
};

Loops #

// Classic for — when you need the index
List<String> buah = ['apple', 'orange', 'mango'];
for (int i = 0; i < buah.length; i++) {
  print('$i: ${buah[i]}');
}

// for-in — when you only need the elements
for (String b in buah) {
  print(b);
}

// forEach with a lambda — functional style
buah.forEach((b) => print(b));

// while — when the stop condition isn't known upfront
int n = 1;
while (n < 100) {
  n *= 2;
}
print(n); // 128

// do-while — the body runs at least once
int input;
do {
  input = bacaInput();
} while (input < 0);
// ANTI-PATTERN: using a classic for when you don't need the index
for (int i = 0; i < buah.length; i++) {
  print(buah[i]); // i isn't used for anything except element access
}

// CORRECT: use for-in
for (String b in buah) {
  print(b);
}

break, continue, and Labels #

// break — stop the loop
for (int i = 0; i < 10; i++) {
  if (i == 5) break;
  print(i); // prints 0 through 4
}

// continue — skip this iteration, go to the next
for (int i = 0; i < 10; i++) {
  if (i % 2 == 0) continue;
  print(i); // prints 1, 3, 5, 7, 9
}

// label — for break/continue in nested loops
luarLoop:
for (int i = 0; i < 3; i++) {
  for (int j = 0; j < 3; j++) {
    if (j == 1) break luarLoop; // exit both loops
    print('$i,$j');
  }
}

Functions #

Functions in Dart are first-class citizens — meaning functions can be stored in variables, passed as parameters, and returned from other functions. This opens the door to a very powerful functional programming style.

Declarations and Parameters #

// Function with an explicit return type
int tambah(int a, int b) {
  return a + b;
}

// Arrow function — for single-expression functions
int kali(int a, int b) => a * b;

// Required positional parameters
void sapa(String nama, int umur) {
  print('Hello $nama, age $umur years');
}

// Optional positional parameters — wrapped in []
void sapaDenganGelar(String nama, [String? gelar]) {
  print('Hello ${gelar != null ? "$gelar " : ""}$nama');
}

// Named parameters — wrapped in {}
void buatProfil({
  required String nama,   // required: must be provided
  int umur = 0,           // default value: optional
  String? kota,           // nullable: optional
}) {
  print('$nama, $umur years old, ${kota ?? "unknown city"}');
}

void main() {
  sapa('Budi', 25);
  sapaDenganGelar('Siti', 'Dr.');
  buatProfil(nama: 'Andi', umur: 30, kota: 'Bandung');
  buatProfil(nama: 'Rini'); // umur = 0, kota = null
}
// ANTI-PATTERN: positional parameters for functions with many arguments
void buatOrder(String nama, String alamat, int qty, double harga, bool express) {
  // When calling: buatOrder('Budi', 'Jl. Merdeka 1', 2, 150000, true)
  // Unclear which argument is express and which is qty
}

// CORRECT: use named parameters for clarity
void buatOrder({
  required String nama,
  required String alamat,
  required int qty,
  required double harga,
  bool express = false,
}) {}
// Call: buatOrder(nama: 'Budi', alamat: 'Jl. Merdeka 1', qty: 2, harga: 150000, express: true)
// Every argument's purpose is clear

Functions as Values #

// Storing a function in a variable
int Function(int, int) operasi = tambah;
print(operasi(3, 4)); // 7

operasi = (a, b) => a * b; // switch to a multiplication function
print(operasi(3, 4)); // 12

// Functions as parameters (higher-order functions)
List<int> angka = [1, 2, 3, 4, 5];

// map: transform every element
List<int> dikuadratkan = angka.map((n) => n * n).toList();
print(dikuadratkan); // [1, 4, 9, 16, 25]

// where: filter elements
List<int> genap = angka.where((n) => n % 2 == 0).toList();
print(genap); // [2, 4]

// reduce: aggregation
int total = angka.reduce((acc, n) => acc + n);
print(total); // 15

Classes and Objects #

Dart is thoroughly object-oriented — even int and bool are classes. This provides a consistency you won’t find in languages like Java that distinguish between primitive types and object types.

Defining Classes #

class Produk {
  // Properties (fields)
  final String id;
  String nama;
  double harga;
  int stok;

  // Main constructor
  Produk({
    required this.id,
    required this.nama,
    required this.harga,
    this.stok = 0,
  });

  // Named constructor — for different creation scenarios
  Produk.kosong()
      : id = '',
        nama = '',
        harga = 0,
        stok = 0;

  // Factory constructor — for complex creation logic
  factory Produk.dariJson(Map<String, dynamic> json) {
    return Produk(
      id: json['id'] as String,
      nama: json['nama'] as String,
      harga: (json['harga'] as num).toDouble(),
      stok: json['stok'] as int? ?? 0,
    );
  }

  // Getter — a computed property
  bool get tersedia => stok > 0;
  String get ringkasan => '$nama (Rp${harga.toStringAsFixed(0)})';

  // Method
  void tambahStok(int jumlah) {
    if (jumlah <= 0) throw ArgumentError('Quantity must be positive');
    stok += jumlah;
  }

  // Override toString for debugging
  @override
  String toString() => 'Produk($id: $nama, stok: $stok)';
}

Inheritance and Polymorphism #

abstract class Bentuk {
  // Abstract method — must be implemented by subclasses
  double hitungLuas();
  double hitungKeliling();

  // Concrete method — can be inherited directly
  void tampilkan() {
    print('Area: ${hitungLuas()}, Perimeter: ${hitungKeliling()}');
  }
}

class Persegi extends Bentuk {
  final double sisi;
  Persegi(this.sisi);

  @override
  double hitungLuas() => sisi * sisi;

  @override
  double hitungKeliling() => 4 * sisi;
}

class Lingkaran extends Bentuk {
  final double jariJari;
  Lingkaran(this.jariJari);

  @override
  double hitungLuas() => 3.14159 * jariJari * jariJari;

  @override
  double hitungKeliling() => 2 * 3.14159 * jariJari;
}

void main() {
  List<Bentuk> bentuk = [Persegi(5), Lingkaran(3)];
  for (var b in bentuk) {
    b.tampilkan(); // polymorphism: the right method is called automatically
  }
}
classDiagram
    class Bentuk {
        <<abstract>>
        +hitungLuas() double
        +hitungKeliling() double
        +tampilkan() void
    }
    class Persegi {
        +sisi double
        +hitungLuas() double
        +hitungKeliling() double
    }
    class Lingkaran {
        +jariJari double
        +hitungLuas() double
        +hitungKeliling() double
    }
    Bentuk <|-- Persegi
    Bentuk <|-- Lingkaran

Collections #

Dart provides three main collection types — List, Set, and Map — all generic and null-safe. Choosing the right type for your data needs is an important design decision.

// List — ordered collection, elements can be duplicated
List<String> kota = ['Jakarta', 'Bandung', 'Surabaya'];
kota.add('Medan');
kota.insert(1, 'Yogyakarta');       // insert at index 1
kota.removeWhere((k) => k.length > 7); // remove cities with names longer than 7 characters
print(kota.first);                  // first element
print(kota.last);                   // last element
print(kota.length);

// Set — unique, unordered collection, set operations
Set<String> tags = {'dart', 'flutter', 'google'};
tags.add('dart');   // no duplication, still 3 elements
Set<String> lebih = {'dart', 'mobile', 'web'};

print(tags.intersection(lebih)); // {dart}
print(tags.union(lebih));        // {dart, flutter, google, mobile, web}
print(tags.difference(lebih));   // {flutter, google}

// Map — key-value pairs
Map<String, int> skorSiswa = {
  'Budi': 90,
  'Siti': 85,
  'Andi': 92,
};
skorSiswa['Rini'] = 88;
print(skorSiswa['Budi']);              // 90
print(skorSiswa['Tono'] ?? 0);        // 0 (default if absent)
print(skorSiswa.keys.toList());       // all names
print(skorSiswa.values.toList());     // all scores
print(skorSiswa.entries.toList());    // all pairs

// Iterating a Map
skorSiswa.forEach((nama, skor) {
  print('$nama: $skor');
});

Collection If and Collection For #

Dart has special syntax for building collections conditionally or iteratively:

bool tampilkanBonus = true;
List<String> menu = [
  'Fried Rice',
  'Fried Noodles',
  if (tampilkanBonus) 'Free Ice Cream',  // collection if
];

List<int> angka = [1, 2, 3];
List<int> dikali = [
  for (int n in angka) n * 10,  // collection for: [10, 20, 30]
];
CollectionOrderedDuplicatesAccessBest for
List<T>IndexOrder matters, fast access
Set<T>IterationUniqueness, set operations
Map<K,V>Unique keysKeyFast lookup by key

Exception Handling #

Exceptions in Dart use the familiar try-catch-finally pattern, with an added on block to catch specific exception types separately — cleaner than using instanceof inside a catch.

// Dart's basic exception hierarchy
// Object
//   └── Exception (thrown by runtime conditions)
//         ├── FormatException
//         ├── IOException
//         └── ...
//   └── Error (programmer bugs, should not be caught)
//         ├── ArgumentError
//         ├── RangeError
//         └── ...

double bagi(double a, double b) {
  if (b == 0) throw ArgumentError('Divisor cannot be zero');
  return a / b;
}

void main() {
  try {
    print(bagi(10, 2));   // 5.0
    print(bagi(10, 0));   // throws ArgumentError
  } on ArgumentError catch (e) {
    // catch a specific type
    print('Wrong argument: ${e.message}');
  } on FormatException catch (e, stackTrace) {
    // e is the exception, stackTrace is the call stack
    print('Wrong format: $e');
    print(stackTrace);
  } catch (e) {
    // catch all other types
    print('Unknown error: $e');
  } finally {
    // always runs, whether there's an exception or not
    print('try-catch block finished');
  }
}
// ANTI-PATTERN: catching everything with an empty catch
try {
  prosesData();
} catch (e) {
  // silent — this hides bugs that should be known
}

// ANTI-PATTERN: catching Error (not Exception)
try {
  var list = [1, 2, 3];
  print(list[10]); // RangeError
} catch (e) {
  print('Catch everything including Error'); // Error should not be caught
}

// CORRECT: catch specific Exceptions, let Errors bubble up
try {
  prosesData();
} on FormatException catch (e) {
  // handle a bad format
  print('Invalid data: ${e.message}');
} on IOException catch (e) {
  // handle I/O errors
  print('Failed to read file: $e');
}
Don’t catch Error (like RangeError, ArgumentError, StackOverflowError) — these are signals of bugs in the code, not recoverable runtime conditions. Let them crash so they can be fixed. What’s worth catching is Exception.

Asynchronous Programming #

Dart uses a single-threaded event loop model — similar to JavaScript. All I/O operations (file reads, HTTP requests, database queries) run asynchronously so they don’t block the main thread. Future and Stream are the two main abstractions for working with asynchronous code.

Future and async/await #

// Future<T> represents a value that will be available in the future
Future<String> ambilData(String url) async {
  // Simulate an HTTP request
  await Future.delayed(Duration(seconds: 2));
  return 'Data from $url';
}

// async/await makes asynchronous code read like synchronous code
void main() async {
  print('Starting to fetch data...');

  // Waiting for one Future
  String hasil = await ambilData('https://api.example.com/data');
  print(hasil);

  // Running several Futures in parallel
  List<Future<String>> semuaRequest = [
    ambilData('https://api.example.com/user'),
    ambilData('https://api.example.com/produk'),
    ambilData('https://api.example.com/order'),
  ];

  List<String> semuaHasil = await Future.wait(semuaRequest);
  semuaHasil.forEach(print);

  print('Done.');
}
// ANTI-PATTERN: sequential await when you could go parallel
String user    = await ambilUser();     // wait for this first
String produk  = await ambilProduk();   // only then run this
String order   = await ambilOrder();    // total time = sum of all times

// CORRECT: run in parallel with Future.wait
var results = await Future.wait([
  ambilUser(),
  ambilProduk(),
  ambilOrder(),
]);
// Total time = the slowest of the three

Stream #

Stream is a sequence of asynchronous events — like Future but able to deliver many values over time:

// A simple stream with a generator
Stream<int> hitungMundur(int dari) async* {
  for (int i = dari; i >= 0; i--) {
    await Future.delayed(Duration(seconds: 1));
    yield i; // emit one value
  }
}

void main() async {
  // Listening to a Stream with await for
  await for (int angka in hitungMundur(5)) {
    print(angka); // 5, 4, 3, 2, 1, 0 — one per second
  }

  print('Done!');
}
sequenceDiagram
    participant Main
    participant Future
    participant EventLoop

    Main->>EventLoop: await ambilData()
    Note over Main: Main "sleeps", not blocking
    EventLoop->>Future: run the async operation
    Future-->>EventLoop: done, data available
    EventLoop-->>Main: resume, data received
    Main->>Main: continue execution

Imports and Modularization #

Dart organizes code into libraries — every .dart file is automatically a library. import is used to use code from other libraries.

// Import Dart standard libraries
import 'dart:math';           // math functions
import 'dart:convert';        // JSON, UTF-8, etc.
import 'dart:io';             // files, sockets, HTTP server

// Import packages from pub.dev
import 'package:http/http.dart' as http;    // alias to avoid conflicts
import 'package:path/path.dart';

// Import local files
import 'produk.dart';
import '../utils/formatter.dart';

// Import with show — only import what you need
import 'dart:math' show sqrt, pi;

// Import with hide — import everything except what's listed
import 'dart:math' hide Random;

void main() async {
  // Using an alias to avoid name ambiguity
  var response = await http.get(Uri.parse('https://api.example.com'));
  var data = jsonDecode(response.body);
  print(sqrt(pi)); // from dart:math
}
// ANTI-PATTERN: import without an alias when there's potential for name conflicts
import 'package:http/http.dart';
import 'package:dio/dio.dart';

// Now 'Response' could be ambiguous — from http or dio?
Response r = await get(Uri.parse('...')); // ✗ compile error

// CORRECT: use aliases
import 'package:http/http.dart' as http;
import 'package:dio/dio.dart' as dio;

http.Response r1 = await http.get(Uri.parse('...'));
dio.Response r2 = await Dio().get('...');

Summary #

  • The entry point of every Dart program is void main() — without it, the program can’t run.
  • Null safety is a core feature of modern Dart — all variables are non-nullable by default, use ? only for variables that can genuinely be null, and the ??, ?., ??= operators to handle them safely.
  • var vs explicit types — use var when the type is obvious from the initial value, explicit types when it isn’t or for public APIs.
  • final vs constfinal for values that don’t change after being set (runtime), const for values already known at compile time.
  • Named parameters are preferred for functions with more than two parameters — calls become much easier to read.
  • Collection if and collection for make building List/Set/Map conditionally extremely concise and expressive.
  • Catch Exception, let Error propagate — Errors are programmer bugs that should be fixed, not conditions to handle at runtime.
  • Future.wait() for parallelism — don’t await one by one when several operations can run concurrently.
  • import ... as alias to avoid name conflicts between libraries, especially when using many external packages.

← Previous: Installation   Next: Comments →

About | Author | Content Scope | Editorial Policy | Privacy Policy | Disclaimer | Contact