Anxiety Assertion

Sebagian besar codebase produksi termasuk dalam salah satu dari dua kubu. Kubu A memperlakukan assert sebagai bumbu dekoratif, menaburkannya di setiap baris lain hingga fungsi tersebut terbaca seperti kontrak hukum yang ditulis oleh pengacara paranoid. Kubu B memperlakukan assertion sebagai roda bantu khusus pengembangan, menghapus semuanya saat build time dan berharap kode berfungsi di produksi karena tes pernah lulus sekali.

Kedua kubu salah. Pertanyaannya bukan apakah harus melakukan assert. Pertanyaannya adalah apa sebenarnya arti dari sebuah assertion.

Sebuah assertion bukanlah error handling. Bukan validasi input. Bukan saran yang sopan. Assertion adalah klaim bahwa sesuatu tidak mungkin terjadi. Jika assertion tersebut aktif, model mental Anda tentang program sudah rusak. Perbedaan itu menentukan segalanya tentang di mana assertion seharusnya berada dan berapa banyak yang harus Anda tulis.

Assertion untuk Invariants, Bukan Error

Ketika pengguna mengirimkan age negatif ke API Anda, itu adalah error. Error diharapkan terjadi. Error layak mendapatkan penanganan yang nyata, logging, dan pesan yang ditujukan ke pengguna. Ketika perhitungan internal Anda menghasilkan jumlah baris database yang negatif setelah query yang seharusnya berhasil, itu adalah pelanggaran invariant. Itu seharusnya tidak pernah terjadi. Itulah sebabnya assertion ada.

Ini terdengar jelas sampai Anda membaca kode produksi. Saya pernah melihat fungsi yang meng-assert sebuah string non-kosong, lalu tiga baris kemudian memeriksa if (!str) dan melempar formatted exception. Developer tersebut menggunakan kedua tools untuk kondisi yang sama karena mereka tidak pernah memutuskan mana yang merupakan kontrak yang sebenarnya.

Inilah aturannya. Jika kondisi tersebut dapat dipicu oleh input eksternal, itu bukan assertion. Jika hanya dapat dipicu oleh bug di kode Anda sendiri, maka itu adalah assertion.

def process_payment(user_id: str, amount_cents: int) -> Receipt:
    # NOT an assertion. Users or upstream services can send bad data.
    if amount_cents <= 0:
        raise ValueError("amount_cents must be positive")

    # NOT an assertion. The user_id comes from the outside world.
    if not user_id:
        raise ValueError("user_id is required")

    receipt = _charge_card(user_id, amount_cents)

    # THIS is an assertion. If charge_card returned None after
    # succeeding, our understanding of the universe is wrong.
    assert receipt is not None, "charge_card succeeded but returned None"

    # THIS is an assertion. A receipt with zero items after a
    # successful charge means our internal logic is broken.
    assert len(receipt.items) > 0, "receipt has no items after successful charge"

    return receipt

Dua pemeriksaan pertama menjaga boundary. Dua terakhir menjaga konsistensi internal sistem. Mencampuradukkannya menciptakan kebingungan tentang siapa yang bertanggung jawab atas apa.

Batas Tiga Assertion

Jika Anda menemukan diri Anda menulis lebih dari tiga assertion dalam satu fungsi, Anda memiliki salah satu dari dua masalah. Entah fungsi Anda melakukan terlalu banyak hal, atau invariants Anda terlalu kabur untuk ditegakkan.

Sebuah fungsi dengan dua belas assertion bukanlah defensif. Itu adalah ketidakpastian. Penulisnya tidak mempercayai kode yang memanggilnya, kode yang dipanggilnya, atau data yang mengalir di antara keduanya. Ketidakpastian itu harus diselesaikan dengan refactor, bukan dengan menambahkan lebih banyak pernyataan assert.

Batas praktis berasal dari apa yang dapat developer pegang di kepala mereka. Sebuah fungsi seharusnya memiliki satu kontrak yang jelas. Kontrak tersebut mengimplikasikan sejumlah kecil invariants. Jika Anda membutuhkan selusin assertion untuk merasa aman, fungsi Anda mungkin telah menyerap tanggung jawab yang seharusnya berada di tempat lain.

Pisahkan fungsinya. Ekstrak bagian yang mentransformasi data. Ekstrak bagian yang memanggil external services. Berikan setiap fungsi yang diekstrak set invariants kecilnya sendiri. Tiga assertion per fungsi adalah lampu peringatan. Lima adalah ban kempes.

Assertion di Production: Aktif atau Mati?

Berbagai bahasa membuat pilihan yang berbeda. Python menghapus pernyataan assert ketika Anda menjalankan dengan flag -O. Kompiler C dan C++ secara rutin menghapus assertion di release build. JavaScript sama sekali tidak memiliki assert bawaan. Anda bisa polyfill atau menggunakan library yang tetap aktif di production.

Ini menciptakan dilema yang nyata. Jika Anda menghapus assertion, Anda kehilangan safety net tepat ketika Anda paling membutuhkannya. Bug yang hanya muncul di production akan secara diam-diam merusak data alih-alih gagal dengan cepat. Jika Anda menyimpannya, Anda berisiko men-crash proses production karena suatu kondisi yang, meskipun secara teoritis tidak mungkin, sebenarnya tidak fatal.

Jawabannya tergantung pada biaya kelanjutan. Jika melanggar invariant berarti operasi berikutnya akan merusak database atau membocorkan data sensitif, assertion tersebut harus men-crash proses. Menghentikan dengan keras lebih baik daripada pelanggaran yang diam-diam. Jika melanggar invariant berarti entri log yang sedikit salah atau glitch UI yang minor, log dan lanjutkan.

// This should probably crash. Continuing with a null user
// after auth succeeded is a security hole waiting to happen.
assert(user !== null, "auth middleware returned null user after success");

// This should probably not crash. A stale cache timestamp
// is annoying but not dangerous.
if (cache.timestamp > Date.now()) {
  logger.warn("cache timestamp is in the future, ignoring");
}

Tidak setiap invariant layak mendapatkan sikap yang sama. Pelajari untuk membedakan antara “ini harus dihentikan” dan “ini aneh tapi masih bisa bertahan.”

Apa yang Kami Coba yang Tidak Berhasil

Di awal satu proyek, kami mencoba meng-assert setiap precondition fungsi. Setiap argumen diperiksa untuk null, type, range, dan format. Hasilnya bisa diprediksi. Tes lulus dengan indah. Production crash pertama kali API pihak ketiga mengembalikan field sebagai string alih-alih number.

Masalahnya bukan pada assertion. Masalahnya adalah kami meng-assert data dari luar kendali kami, lalu mengompilasi dengan assertion diaktifkan di production. Respons eksternal yang malformed membunuh proses kami alih-alih disanitasi dan ditangani. Kami telah membangun sistem yang konsisten secara internal dan rapuh secara eksternal.

Kami belajar untuk memisahkan boundary dari interior. Di boundary, parse dan validate dengan agresif. Ubah kekacauan eksternal menjadi kepastian internal. Di dalam boundary, assert invariants yang mendefinisikan kepastian itu. Assertion-nya tetap ada. Validasi input dipindahkan ke explicit parsing functions yang mengembalikan Result types alih-alih melempar.

Checklist Praktis

Sebelum Anda menambahkan assertion, jalankan daftar ini:

  1. Bisakah input eksternal memicu ini? Jika ya, gunakan validation, bukan assertion.
  2. Jika ini aktif di production, apakah proses harus dihentikan? Jika tidak, log warning sebagai gantinya.
  3. Apakah fungsi ini sudah memiliki tiga atau lebih assertion? Jika ya, pertimbangkan untuk refactor sebelum menambahkan yang lain.
  4. Apakah assertion ini masih masuk akal bagi seseorang yang membaca kode dalam enam bulan? Assertion yang obscure dihapus saat refactor. Yang jelas bertahan.

Assertion adalah alat komunikasi sebanyak alat keselamatan. Mereka memberi tahu developer berikutnya, “kondisi ini secara desain tidak mungkin terjadi.” Jika kondisi tersebut sebenarnya tidak mustahil secara desain, assertion tersebut berbohong. Dan kebohongan dalam kode produksi mahal harganya.

FAQ

Haruskah saya assert pada argumen fungsi?

Hanya jika caller-nya juga kode Anda dan argumen tersebut adalah produk dari internal logic, bukan input eksternal. Fungsi Public API harus validate. Fungsi private helper dapat meng-assert invariants tentang nilai yang diterimanya.

Bagaimana dengan TypeScript? Sudah menangkap null di compile time.

Type system TypeScript adalah assertion layer yang powerful, tapi menghilang saat runtime. Gunakan untuk segala sesuatu yang dapat dibuktikan oleh compiler. Tambahkan runtime assertions untuk celahnya: API responses, deserialized data, dan setiap as cast yang melewati type checker.

Apakah assertion merusak performance?

Di sebagian besar bahasa, assertion yang ditempatkan dengan baik membutuhkan microseconds. Jika Anda meng-assert di dalam tight loop yang memproses jutaan item, pindahkan assertion di luar loop. Periksa invariant pada batch, bukan pada setiap elemen.

Haruskah saya menulis custom assert functions?

Hanya ketika pesan assertion bawaan tidak membantu. Sebuah assertNonEmpty custom yang mencetak actual array length lebih berguna daripada generic assert len(items) > 0 yang crash tanpa konteks. Jadikan kecil. Jangan membangun assertion framework.