Sejauh ini setiap ekspresi yang kita tulis menghasilkan nilai dengan tipe tertentu: 5 bertipe i32, "Halo" bertipe &str, dan pemanggilan Vec::new() bertipe Vec<T>. Namun ada kelompok kode yang sifatnya berbeda: panic! menghentikan program, process::exit() mematikan proses, dan loop {} tanpa break berjalan selamanya — tidak satu pun dari mereka pernah menghasilkan nilai untuk dipakai baris berikutnya, karena baris berikutnya tidak pernah terjadi. Rust memodelkan situasi ini dengan never type, ditulis !. Pemahaman tipe ini menjelaskan keanehan yang mungkin sudah kita temui: mengapa panic! boleh duduk berdampingan dengan &str di dalam satu match, padahal aturan tipe Rust terkenal ketat. Jawabannya ada pada kemampuan ! menyatu dengan tipe apa pun.

Ekspresi yang Tidak Pernah Menghasilkan Nilai #

Never type menjawab pertanyaan sederhana: apa tipe dari ekspresi yang tidak pernah selesai dievaluasi? Ketika panic! dieksekusi, stack mulai dibongkar (unwinding) dan program berhenti; ketika process::exit(101) berjalan, proses langsung dimatikan sistem operasi. Ekspresi seperti ini dikatakan diverging — mengalir keluar dari jalur normal program dan tidak pernah kembali membawa nilai. Rust memberi seluruh ekspresi diverging tersebut tipe !.

Dua bukti penting mengikuti definisi ini. Pertama, fungsi yang selalu mengakhiri program boleh mendeklarasikan tipe kembalian !, dan deklarasi tersebut sekaligus menjanjikan bahwa fungsi tidak punya jalur yang kembali normal. Kedua — dan inilah yang paling sering kita manfaatkan — ! dapat menyatu (unify) dengan tipe apa pun. Karena nilai bertipe ! tidak pernah benar-benar ada saat program berjalan, kompilator membiarkannya berpura-pura menjadi tipe apa pun yang dibutuhkan konteksnya.

! Menyatukan Tipe pada match #

Aturan match menuntut semua arm menghasilkan tipe yang sama. Di sinilah ! bekerja: arm yang diverging tidak perlu mencocokkan tipe arm lainnya karena arm tersebut tidak pernah menghasilkan nilai. Pola yang paling umum adalah arm terakhir yang memanggil panic!:

rust
fn main() {
    let grade = "B";

    let description: &str = match grade {
        "A" => "sangat baik",
        "B" => "baik",
        "C" => "cukup",
        _ => panic!("nilai tidak dikenal: {}", grade),
    };

    println!("Deskripsi nilai: {}", description);
}

Output yang Dihasilkan:

text
Deskripsi nilai: baik

Mari kita bedah kode di atas:

  1. Baris 4 (let description: &str = match grade {): hasil match diikat ke variabel bertipe &str. Seluruh arm harus menyepakati tipe tersebut.
  2. Baris 5-7 ("A" => "sangat baik", dst.): tiga arm pertama menghasilkan &str karena setiap literal teks bertipe &str.
  3. Baris 8 (_ => panic!("nilai tidak dikenal: {}", grade),): arm penangkap sisa ini menghasilkan tipe !, bukan &str. Kompilator menerima perbedaan ini karena panic! diverging — tidak ada nilai yang perlu dikonversi. Ketika grade di luar A, B, dan C muncul, program berhenti di baris ini dan variabel description tidak pernah dibuat.
  4. Baris 11 (println!("Deskripsi nilai: {}", description);): baris ini hanya tercapai bila arm 5-7 yang menang, sehingga description dijamin bertipe &str dan aman dicetak.

Pola yang sama bekerja pada Option: arm None => panic!("konfigurasi belum diatur") pada hasil pembacaan konfigurasi, atau arm Err(message) => panic!(...) pada hasil parsing — semuanya berdiri di atas unifikasi !.

Ketika Arm Tidak Divergen #

Mekanisme ini mudah disalahpami sebagai “arm terakhir bebas bertipe apa pun”. Padahal izin khusus hanya berlaku untuk ekspresi diverging. Begitu arm terakhir diganti ekspresi biasa yang tipe berbeda, kompilator langsung menolak. Perhatikan percobaan mengganti panic! dengan format!:

rust
fn main() {
    let grade = "B";

    let description: &str = match grade {
        "A" => "sangat baik",
        "B" => "baik",
        _ => format!("nilai tidak dikenal: {}", grade),
    };

    println!("Deskripsi nilai: {}", description);
}

Program di atas gagal dikompilasi:

text
error[E0308]: mismatched types
 --> main.rs:7:14
  |
7 |         _ => format!("nilai tidak dikenal: {}", grade),
  |              ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ expected `&str`, found `String`

error: aborting due to 1 previous error

For more information about this error, try `rustc --explain E0308`.

Pesan E0308 menegaskan batasnya: format! adalah ekspresi biasa yang menghasilkan String dan bisa saja benar-benar tereksekusi, sehingga arm tersebut harus cocok dengan tipe arm lain (&str). Kesimpulannya tajam: ! menyatu dengan semua tipe justru karena ! dijamin tidak pernah menghasilkan nilai; String tidak punya jaminan itu, jadi String harus mengikuti aturan umum.

Fungsi Bertipe Kembalian ! #

Menulis fungsi sendiri yang diverging memakai anotasi -> !. Tipe kembalian ini menjanjikan kepada kompilator bahwa semua jalur fungsi berujung pada penghentian program. Janji tersebut diperiksa: fungsi bertipe ! yang masih punya jalur kembali normal akan ditolak kompilator. Contoh penerapannya pada penanganan kondisi fatal:

rust
fn abort_with_error(message: &str) -> ! {
    println!("ERROR FATAL: {}", message);
    std::process::exit(101);
}

fn main() {
    let disk_space_gb: Option<u32> = None;

    let space = match disk_space_gb {
        Some(value) => value,
        None => abort_with_error("ruang disk tidak terdeteksi"),
    };

    println!("Ruang disk tersedia: {} GB", space);
}

Karena disk_space_gb bernilai None, program berhenti di tengah jalan. Kompilasi dan eksekusinya:

bash
rustc --edition 2021 main.rs -o never-demo && ./never-demo; echo "exit code: $?"

Output yang Dihasilkan:

text
ERROR FATAL: ruang disk tidak terdeteksi
exit code: 101

Mari kita bedah kode dan hasilnya:

  1. Baris 1 (fn abort_with_error(message: &str) -> ! {): tipe kembalian ! menyatakan fungsi ini tidak pernah kembali membawa nilai. Signature semacam ini membuat fungsi dapat ditempatkan pada posisi yang menuntut tipe apa pun.
  2. Baris 3 (std::process::exit(101);): pemanggilan yang mematikan proses dengan exit code 101. Ekspresi ini bertipe !, dan karena baris inilah satu-satunya jalur fungsi, janji -> ! terpenuhi.
  3. Baris 11 (None => abort_with_error(...)): arm None memanggil fungsi bertipe !, sehingga tipe match ditentukan seluruhnya oleh arm Some — u32 — dan variabel space dijamin u32 di baris selanjutnya.
  4. Output program: teks “ERROR FATAL: ruang disk tidak terdeteksi” tercetak, baris println! di main tidak pernah tercapai, dan proses berakhir dengan exit code 101 yang terbaca lewat echo "exit code: $?".

continue dan break dalam Posisi Ekspresi #

Unifikasi ! juga bekerja pada continue dan break, yang sama-sama mengalir keluar dari ekspresi sebelum nilai dihasilkan. Kedua kata kunci ini memungkinkan pola “saring di tengah ekspresi”:

rust
fn main() {
    let sensor_readings = [21, 0, 19, 0, 24];
    let mut valid_total = 0;

    for reading in sensor_readings {
        let value: i32 = if reading == 0 {
            continue;
        } else {
            reading
        };
        valid_total += value;
    }

    println!("Total pembacaan valid: {}", valid_total);
}

Output yang Dihasilkan:

text
Total pembacaan valid: 64

Mari kita amati baris kuncinya:

  1. Baris 6-10 (let value: i32 = if reading == 0 { ... } else { reading };): cabang if berisi continue; yang bertipe !, sedangkan cabang else berisi reading yang bertipe i32. Kedua cabang tidak perlu bertipe sama: cabang pertama diverging, jadi tipe seluruh ekspresi if mengikuti cabang else, yaitu i32.
  2. Baris 7 (continue;): saat pembacaan sensor bernilai 0, continue melempar eksekusi ke iterasi berikutnya. Pernyataan let value untuk iterasi tersebut tidak pernah menyelesaikan evaluasinya, sehingga tidak ada nilai ! yang perlu menjadi i32.
  3. Hasil akhir: pembacaan 0 dilewati dua kali, dan 21 + 19 + 24 menghasilkan 64 — sesuai dengan output yang tercetak.

Ringkasan #

Never type ! adalah tipe untuk ekspresi diverging — panic!, process::exit, loop {} tanpa break, continue, dan break — yang dijamin tidak pernah menghasilkan nilai. Jaminan inilah yang memberi ! kemampuan menyatu dengan tipe apa pun: arm match berisi panic! diterima di samping arm &str, cabang if berisi continue boleh dipasangkan dengan cabang i32, dan fungsi bertipe kembalian ! dapat duduk di posisi nilai bertipe apa pun yang dijanjikan. Ketika arm diverging diganti ekspresi biasa seperti format!, perlindungan hilang dan E0308 muncul kembali seperti biasa. Kita sekarang beralih dari tipe tanpa nilai ke tipe tanpa ukuran tetap: sized dan unsized pada Chapter berikutnya.