[{"data":1,"prerenderedAt":309},["ShallowReactive",2],{"tag-labels":3,"site-header":4,"article-jawaban-kumpulkan-semua-masalah-dulu-baru-lapor-id":69},{},{"featuredPost":5,"navigations":26},{"id":6,"title":7,"title_en":8,"slug":9,"excerpt":10,"excerpt_en":8,"has_translation":11,"published_at":12,"reading_time":13,"hidden_from_listing":11,"tags":14,"thumbnail_image":8,"og_image":19,"series":20,"authors":25},110,"Ketika Pilih Framework Cuma Biar Kelihatan Beda",null,"ketika-pilih-framework-cuma-biar-kelihatan-beda","Terakhir kali kamu milih framework atau library baru, itu karena masalahmu butuh alat itu, atau karena alatnya kedengaran keren dan belum banyak yang pakai? Pertanyaan ini nggak enak dijawab. Soalnya keputusan teknologi sering diambil dengan alasan yang nggak pernah ditulis di dokumen teknis mana pun.",false,"2026-10-06T15:25:21.000Z",5,[15,16,17,18],"karier","framework","opini","pengalaman","\u002Fog\u002Fketika-pilih-framework-cuma-biar-kelihatan-beda.png",{"id":21,"title":22,"title_en":23,"slug":24},9,"Artikel","Articles","all",[],[27,43,64],{"collection":28,"item":29},"series",{"__typename":28,"title":22,"title_en":23,"slug":24,"children_series":30},[31,35,39],{"title":32,"title_en":33,"slug":34},"Kecerdasan Artifisial","Artificial Intelligence","kecerdasan-artifisial",{"title":36,"title_en":37,"slug":38},"Konsep Dasar Pemrograman","Programming Fundamentals","konsep-dasar-pemrograman",{"title":40,"title_en":41,"slug":42},"Algoritma dan Struktur Data","Algorithms and Data Structures","algoritma-struktur-data",{"collection":28,"item":44},{"__typename":28,"title":45,"title_en":45,"slug":46,"children_series":47},"Tutorial","tutorial",[48,52,56,60],{"title":49,"title_en":50,"slug":51},"Belajar Python","Learning Python","belajar-python",{"title":53,"title_en":54,"slug":55},"Belajar Golang","Learning Go","belajar-golang",{"title":57,"title_en":58,"slug":59},"Belajar Java","Learning Java","belajar-java",{"title":61,"title_en":62,"slug":63},"Belajar Pascal","Learning Pascal","belajar-pascal",{"collection":28,"item":65},{"__typename":28,"title":66,"title_en":66,"slug":67,"children_series":68},"Lab","lab",[],{"article":70,"renderedBody":182,"renderedTakeaway":183,"latestArticles":184,"toc":283},{"id":71,"title":72,"title_en":73,"slug":74,"excerpt":75,"excerpt_en":76,"has_translation":77,"published_at":78,"reading_time":79,"hidden_from_listing":77,"tags":80,"thumbnail_image":8,"og_image":84,"series":85,"authors":173,"body_locale":176,"available_locales":177,"body":179,"takeway":180,"prerequisite":181},123,"Jawabannya: Kumpulkan Semua Masalah Dulu, Baru Lapor","The answer: Collect Every Problem First, Then Report","jawaban-kumpulkan-semua-masalah-dulu-baru-lapor","Pesan error yang berguna menjawab di mana, apa, dan harusnya bagaimana — sekaligus, tanpa berhenti di masalah pertama.","A useful error message answers where, what, and what it should be — all at once, without stopping at the first problem.",true,"2026-10-11T23:00:15.000Z",4,[81,82,83,67],"python","error-handling","validasi","\u002Fog\u002Fjawaban-kumpulkan-semua-masalah-dulu-baru-lapor.png",{"id":86,"title":66,"title_en":66,"slug":67,"description":87,"description_en":88,"articles":89,"children_series":172,"parent_series":8},29,"Lab praktik: kerjakan masalahnya dulu, baru lihat jawabannya. Tiap lab punya repo yang bisa di-clone, data seukuran produksi, dan target yang harus kamu capai sendiri. Artikel jawabannya ada di seri ini juga, dan cuma bisa kamu buka dari artikel masalahnya.","Practice labs: work the problem first, then read the answer. Every lab ships with a repo you can clone, production-sized data, and a target you have to hit yourself. The write-up lives in this series too, and it only opens from the problem article.",[90,111,128,144,159],{"id":91,"title":92,"title_en":93,"slug":94,"excerpt":95,"excerpt_en":96,"has_translation":77,"published_at":97,"reading_time":13,"hidden_from_listing":11,"tags":98,"thumbnail_image":8,"og_image":102,"series":103,"authors":104},104,"Query-nya Tinggal 2, Tapi Response-nya Masih 1 Detik","N+1 Was Never the Bottleneck: 41 Queries Down to 2, Still One Second","query-tinggal-2-tapi-response-masih-1-detik","Endpoint feed yang cuma menampilkan 20 tulisan butuh satu detik, dan server database dihujani 41 query. Kamu kerjakan nasihat N+1 yang standar, query-nya turun jadi 2, dan latency-nya nggak bergerak. Ini versi \"apa adanya\" dari masalah itu, plus lab-nya.","A feed endpoint that shows 20 posts takes a full second and hits the database with 41 queries. Apply the standard N+1 advice, get it down to 2 queries, and the latency does not move. This is that problem as it happened, plus the lab.","2026-10-04T09:59:21.000Z",[99,100,101],"database","performance","backend","\u002Fog\u002Fquery-tinggal-2-tapi-response-masih-1-detik.png",{"id":86,"title":66,"title_en":66,"slug":67},[105],{"authors_id":106},{"id":107,"name":108,"slug":109,"role":110,"profile_picture":8},1,"Daffa Izzuddin","izzudd","Writer",{"id":112,"title":113,"title_en":114,"slug":115,"excerpt":116,"excerpt_en":117,"has_translation":77,"published_at":118,"reading_time":79,"hidden_from_listing":11,"tags":119,"thumbnail_image":8,"og_image":123,"series":124,"authors":125},114,"Saldo Naik Empat Kali untuk Satu Pembayaran","One Payment, Four Ledger Rows: An Idempotency Lab","saldo-naik-empat-kali-untuk-satu-pembayaran","Kodenya nggak salah, dan gateway-nya juga nggak salah. Tapi satu pembayaran muncul empat kali di ledger. Coba tebak kenapa — lalu bikin `make check` hijau di lab-nya.","The code is not wrong and the gateway is not wrong, yet one payment shows up four times in the ledger. Guess why, then make `make check` green in the lab.","2026-10-11T13:45:00.000Z",[120,121,122,99,67],"idempotency","webhook","postgres","\u002Fog\u002Fsaldo-naik-empat-kali-untuk-satu-pembayaran.png",{"id":86,"title":66,"title_en":66,"slug":67},[126],{"authors_id":127},{"id":107,"name":108,"slug":109,"role":110,"profile_picture":8},{"id":129,"title":130,"title_en":131,"slug":132,"excerpt":133,"excerpt_en":134,"has_translation":77,"published_at":135,"reading_time":136,"hidden_from_listing":11,"tags":137,"thumbnail_image":8,"og_image":139,"series":140,"authors":141},116,"Webhook-nya Telat, Job Rekonsiliasinya Keburu Jalan","The Webhook Arrived Late. The Reconciliation Job Had Already Run.","webhook-telat-job-rekonsiliasi-keburu-jalan","Jalur webhook sudah idempotent. Lalu ada job rekonsiliasi pagi, dan satu pembayaran tercatat dua kali dari dua jalur yang dua-duanya benar. Coba tebak kuncinya salah di mana.","The webhook path is already idempotent. Then a morning reconciliation job appears, and one payment gets recorded twice from two paths that are both right. Guess where the key is wrong.","2026-10-11T13:44:00.000Z",3,[120,138,121,122,67],"reconciliation","\u002Fog\u002Fwebhook-telat-job-rekonsiliasi-keburu-jalan.png",{"id":86,"title":66,"title_en":66,"slug":67},[142],{"authors_id":143},{"id":107,"name":108,"slug":109,"role":110,"profile_picture":8},{"id":145,"title":146,"title_en":147,"slug":148,"excerpt":149,"excerpt_en":150,"has_translation":77,"published_at":78,"reading_time":79,"hidden_from_listing":11,"tags":151,"thumbnail_image":8,"og_image":154,"series":155,"authors":156},120,"Tiap Nota di Kafe Ini Isinya Pesanan yang Sama","Every Receipt in This Café Lists the Same Order","tiap-nota-isinya-pesanan-yang-sama","Nota tiap meja isinya sama semua, dan totalnya Rp0 — padahal tiap meja cuma pesan satu barang. Coba tebak kenapa.","Every table's receipt holds the same items, and the total is Rp0 — even though each table ordered exactly one thing. Guess why, then make `make check` green. A Python scope and mutation lab.",[81,152,153,67],"scope","mutasi","\u002Fog\u002Ftiap-nota-isinya-pesanan-yang-sama.png",{"id":86,"title":66,"title_en":66,"slug":67},[157],{"authors_id":158},{"id":107,"name":108,"slug":109,"role":110,"profile_picture":8},{"id":160,"title":161,"title_en":162,"slug":163,"excerpt":164,"excerpt_en":165,"has_translation":77,"published_at":78,"reading_time":79,"hidden_from_listing":11,"tags":166,"thumbnail_image":8,"og_image":167,"series":168,"authors":169},122,"Script yang Nggak Bisa Bilang di Mana Salahnya","A Script That Cannot Say Where It Went Wrong","script-yang-tidak-bilang-di-mana-salahnya","Script yang berhenti di baris jelek pertama cuma melaporkan sebagian masalah yang ada. Ini yang muncul di layar sekarang.","A script that stops at the first bad line reports only part of the problem. This is what shows up on screen right now.",[81,82,83,67],"\u002Fog\u002Fscript-yang-tidak-bilang-di-mana-salahnya.png",{"id":86,"title":66,"title_en":66,"slug":67},[170],{"authors_id":171},{"id":107,"name":108,"slug":109,"role":110,"profile_picture":8},[],[174],{"authors_id":175},{"id":107,"name":108,"slug":109,"role":110,"profile_picture":8},"id",[178,176],"en","**Ini artikel jawabannya.** Kalau kamu belum mengerjakan lab 0005 dan masih mau mencoba sendiri, mendingan tutup dulu halaman ini. Sisanya di bawah adalah jawabannya, lengkap dengan angka pengukurannya.\n\n## Angka lengkapnya\n\nDua keadaan, diukur dengan `scripts\u002Fcheck.py` yang sama, di mesin yang sama, tanpa mengubah `data\u002F` atau `scripts\u002Fcheck.py` sedikit pun:\n\n| varian | hijau | catatan |\n|---|---|---|\n| kondisi awal (branch `main`) | **1 dari 7** | cuma A hijau: `periksa()` melempar `int(baris[2])`, dan siapa pun yang memanggilnya berhenti di situ |\n| semua masalah dikumpulkan, nomor baris `start=2`, kode keluar 0\u002F1\u002F2 | **7 dari 7** | — |\n\nAngka konkretnya, kalau kamu mau mencocokkan dengan yang kamu lihat di laptopmu. Di kondisi awal, `beberapa.csv` mencuatkan `ValueError: invalid literal for int() with base 10: 'dua puluh'` dan cuma **0 dari 4** masalah yang dilaporkan. `kosong.csv` malah tanpa keluhan sama sekali, lalu tetap bilang `3 baris diimpor` dengan kode keluar `0`, padahal ada baris tanpa nama di dalamnya. `kolom.csv` meledak dengan `IndexError: list index out of range`, **0 dari 2** masalah. Dan `tidak-ada.csv` memberi `FileNotFoundError` dengan kode keluar `1`, bukan `2`.\n\nDi branch `solusi`, `make demo` menghasilkan satu baris laporan untuk tiap masalah — empat baris untuk `beberapa.csv`, dua untuk `kolom.csv` — dengan kode keluar `0`\u002F`1`\u002F`2` sesuai keadaan.\n\n## Kenapa kondisinya begini\n\n### `periksa()` mencetak, bukan mengembalikan — dan melempar di tengah jalan\n\n```python\numur = int(baris[2])   # ValueError kalau isinya bukan angka\nif umur \u003C 1 or umur > 120:\n    print(\"umur tidak masuk akal, dilewati\")\n```\n\nAda dua cacat di dua baris itu. Pertama, `int()` melempar, dan `main()` nggak menangkapnya: satu baris jelek menghentikan seluruh proses, jadi masalah sesudahnya nggak pernah keburu dilaporkan. Kedua, satu-satunya diagnostik cuma `print` yang nggak menyebut baris maupun kolomnya.\n\nYang bikin jebakan ini licin: komentar di atasnya justru membenarkan dua-duanya — \"Python sudah memberi pesan yang jelas\", \"yang bikin sistem rusak cuma umur\". Kodenya **kelihatan** hati-hati. Padahal aturan yang kamu butuhkan kebalikannya: **satu baris jelek nggak boleh menghentikan seluruh proses.** Baris itu dilaporkan, lalu perulangan jalan terus ke baris berikutnya.\n\n### Nomor baris itu urusan yang memanggil, bukan yang memvalidasi\n\n`periksa()` sebaiknya **mengembalikan daftar masalah** dan nggak tahu apa-apa soal nomor baris. Yang menempelkan nomornya adalah `main()`, lewat `enumerate(pembaca, start=2)`. Angka `2` itu bukan detail sepele: baris 1 di file adalah judulnya, jadi baris data pertama ada di baris 2.\n\nKalau kamu mulai dari `start=1`, angka yang kamu laporkan salah persis satu dari yang dilihat pengguna di editornya. Inilah *off-by-one* yang paling gampang lolos dari mata sendiri, karena layarnya \"kelihatannya benar\". Yang memisahkan keduanya cuma satu hal: nomor baris **di file** itu urusan yang memanggil, sedangkan index data itu urusan yang memvalidasi.\n\n### `IndexError` sama saja belum memvalidasi\n\n`kolom.csv` punya baris dengan 2 kolom dan ada juga yang 4 kolom, padahal judulnya cuma punya 3. Menyentuh `baris[2]` sebelum memastikan jumlah kolomnya cukup cuma memindahkan lokasi crash-nya. **`IndexError` di sini artinya belum memvalidasi** — bukan tanda data yang rumit.\n\nUrutan yang benar: periksa jumlah kolom lebih dulu. Kalau `len(baris)` nggak sama dengan jumlah kolom yang diharapkan, laporkan sebagai satu masalah di kolom `jumlah kolom`, lalu jangan lanjut memeriksa isi baris itu. Kolom per kolom belum bisa dipercaya sebelum jumlahnya cocok.\n\n### Kode keluar adalah bagian dari kontrak\n\nScript seperti ini dipanggil dari `make`, `cron`, atau pipa shell. Pemanggilnya nggak membaca layar; dia membaca *exit code*. \"File tidak ada\" dan \"file ada tapi isinya ada yang salah\" itu dua hal yang beda bagi pemanggil — yang pertama artinya salah pakai, yang kedua artinya datanya perlu diperbaiki.\n\nKarena itu kode keluarnya dibedakan: `0` untuk beres, `1` untuk ada baris bermasalah, `2` untuk cara pakainya yang salah (file nggak ada, argumen kurang). Kondisi awal cuma punya satu kode untuk hampir semua kegagalan, jadi pemanggil otomatis nggak bisa memutuskan harus mengulang, berhenti, atau melanjutkan.\n\n## Bentuk solusinya\n\nValidasi jadi fungsi yang **mengumpulkan**, bukan yang melempar:\n\n```python\ndef periksa(baris):\n    masalah = []\n    if len(baris) != len(KOLOM):\n        masalah.append((\"jumlah kolom\", \"... seharusnya ...\"))\n        return masalah\n    nama, email, umur = (sel.strip() for sel in baris)\n    if not nama:\n        masalah.append((\"nama\", \"kolom nama kosong, padahal harus diisi\"))\n    if \"@\" not in email:\n        masalah.append((\"email\", f\"'{email}' bukan alamat email karena tidak ada tanda '@'\"))\n    try:\n        umur_angka = int(umur)\n    except ValueError:\n        masalah.append((\"umur\", f\"'{umur}' bukan angka — isi dengan bilangan bulat, misalnya 25\"))\n    return masalah\n```\n\nlalu di `main()` nomor barisnya ditempel dari baris file:\n\n```python\nfor nomor, baris in enumerate(pembaca, start=2):   # baris 1 file = judul\n    for kolom, pesan in periksa(baris):\n        sys.stderr.write(f\"baris {nomor}: {kolom} — {pesan}\\n\")\n```\n\nSatu baris yang benar mengembalikan daftar kosong, jadi nggak ada yang dilaporkan. Satu baris yang salah bisa mengembalikan **lebih dari satu** masalah sekaligus — di `beberapa.csv` ada satu baris yang nama kosong **dan** emailnya tanpa `@`. Perhatikan juga bahwa `try\u002Fexcept`-nya kecil dan spesifik, cuma membungkus `int(umur)`. Jangan bungkus semuanya dengan satu `try\u002Fexcept` besar di `main()`: itu memang menghilangkan *stack trace*, tapi sekalian menghentikan program di baris jelek pertama, dan pekerjaanmu balik ke titik awal.\n\n## Yang perlu kamu bawa dari sini\n\nPesan error yang berguna menjawab tiga hal — **di mana**, **apa yang salah**, dan **harusnya bagaimana** — dan jawabannya untuk semuanya sekaligus, bukan cuma yang pertama kebetulan ditemui. Program yang berhenti di masalah pertama cuma menjawab sepertiganya. Sisa waktu yang kamu hemat di situ, kamu bayar mahal di telepon yang berdering.\n\nSatu kebiasaan saja yang berubah: sebelum menulis `raise`, tanya dulu siapa yang akan membaca pesannya.\n","- **Satu baris jelek nggak boleh menghentikan seluruh proses.** Laporkan, lalu lanjut ke baris berikutnya.\n- **Nomor baris di file dimulai dari `enumerate(pembaca, start=2)`** — baris 1 adalah judulnya, bukan data.\n- **`IndexError` itu artinya belum memvalidasi.** Periksa jumlah kolom dulu sebelum menyentuh isi baris.\n- **Kode keluar itu kontrak:** `0` beres, `1` ada baris bermasalah, `2` cara pakainya salah.",{"id":160,"title":161,"slug":163},"\u003Cp>\u003Cstrong>Ini artikel jawabannya.\u003C\u002Fstrong> Kalau kamu belum mengerjakan lab 0005 dan masih mau mencoba sendiri, mendingan tutup dulu halaman ini. Sisanya di bawah adalah jawabannya, lengkap dengan angka pengukurannya.\u003C\u002Fp>\n\u003Ch2 id=\"angka-lengkapnya\">Angka lengkapnya\u003C\u002Fh2>\n\u003Cp>Dua keadaan, diukur dengan \u003Ccode>scripts\u002Fcheck.py\u003C\u002Fcode> yang sama, di mesin yang sama, tanpa mengubah \u003Ccode>data\u002F\u003C\u002Fcode> atau \u003Ccode>scripts\u002Fcheck.py\u003C\u002Fcode> sedikit pun:\u003C\u002Fp>\n\u003Ctable>\n\u003Cthead>\n\u003Ctr>\n\u003Cth>varian\u003C\u002Fth>\n\u003Cth>hijau\u003C\u002Fth>\n\u003Cth>catatan\u003C\u002Fth>\n\u003C\u002Ftr>\n\u003C\u002Fthead>\n\u003Ctbody>\u003Ctr>\n\u003Ctd>kondisi awal (branch \u003Ccode>main\u003C\u002Fcode>)\u003C\u002Ftd>\n\u003Ctd>\u003Cstrong>1 dari 7\u003C\u002Fstrong>\u003C\u002Ftd>\n\u003Ctd>cuma A hijau: \u003Ccode>periksa()\u003C\u002Fcode> melempar \u003Ccode>int(baris[2])\u003C\u002Fcode>, dan siapa pun yang memanggilnya berhenti di situ\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>semua masalah dikumpulkan, nomor baris \u003Ccode>start=2\u003C\u002Fcode>, kode keluar 0\u002F1\u002F2\u003C\u002Ftd>\n\u003Ctd>\u003Cstrong>7 dari 7\u003C\u002Fstrong>\u003C\u002Ftd>\n\u003Ctd>—\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003C\u002Ftbody>\u003C\u002Ftable>\n\u003Cp>Angka konkretnya, kalau kamu mau mencocokkan dengan yang kamu lihat di laptopmu. Di kondisi awal, \u003Ccode>beberapa.csv\u003C\u002Fcode> mencuatkan \u003Ccode>ValueError: invalid literal for int() with base 10: &#39;dua puluh&#39;\u003C\u002Fcode> dan cuma \u003Cstrong>0 dari 4\u003C\u002Fstrong> masalah yang dilaporkan. \u003Ccode>kosong.csv\u003C\u002Fcode> malah tanpa keluhan sama sekali, lalu tetap bilang \u003Ccode>3 baris diimpor\u003C\u002Fcode> dengan kode keluar \u003Ccode>0\u003C\u002Fcode>, padahal ada baris tanpa nama di dalamnya. \u003Ccode>kolom.csv\u003C\u002Fcode> meledak dengan \u003Ccode>IndexError: list index out of range\u003C\u002Fcode>, \u003Cstrong>0 dari 2\u003C\u002Fstrong> masalah. Dan \u003Ccode>tidak-ada.csv\u003C\u002Fcode> memberi \u003Ccode>FileNotFoundError\u003C\u002Fcode> dengan kode keluar \u003Ccode>1\u003C\u002Fcode>, bukan \u003Ccode>2\u003C\u002Fcode>.\u003C\u002Fp>\n\u003Cp>Di branch \u003Ccode>solusi\u003C\u002Fcode>, \u003Ccode>make demo\u003C\u002Fcode> menghasilkan satu baris laporan untuk tiap masalah — empat baris untuk \u003Ccode>beberapa.csv\u003C\u002Fcode>, dua untuk \u003Ccode>kolom.csv\u003C\u002Fcode> — dengan kode keluar \u003Ccode>0\u003C\u002Fcode>\u002F\u003Ccode>1\u003C\u002Fcode>\u002F\u003Ccode>2\u003C\u002Fcode> sesuai keadaan.\u003C\u002Fp>\n\u003Ch2 id=\"kenapa-kondisinya-begini\">Kenapa kondisinya begini\u003C\u002Fh2>\n\u003Ch3 id=\"periksa-mencetak-bukan-mengembalikan-dan-melempar-di-tengah-jalan\">\u003Ccode>periksa()\u003C\u002Fcode> mencetak, bukan mengembalikan — dan melempar di tengah jalan\u003C\u002Fh3>\n\n\u003Cdiv class=\"code-block-wrapper my-6 not-prose rounded-xl border-2 border-black dark:border-gray-600 shadow-neo overflow-hidden bg-[#24292e] text-[#e1e4e8] transition-all\">\n  \u003Cdiv class=\"code-block-header flex items-center justify-between px-4 py-2.5 bg-[#1f2428] border-b-2 border-black dark:border-gray-600 font-mono text-xs font-bold text-gray-300 select-none\">\n    \u003Cdiv class=\"flex items-center gap-2\">\n      \u003Cspan class=\"w-3 h-3 rounded-full bg-[#ff5f56] border border-black\u002F40 inline-block\">\u003C\u002Fspan>\n      \u003Cspan class=\"w-3 h-3 rounded-full bg-[#ffbd2e] border border-black\u002F40 inline-block\">\u003C\u002Fspan>\n      \u003Cspan class=\"w-3 h-3 rounded-full bg-[#27c93f] border border-black\u002F40 inline-block\">\u003C\u002Fspan>\n      \u003Cspan class=\"ml-2 font-mono font-bold text-xs uppercase tracking-wider text-gray-300\">PYTHON\u003C\u002Fspan>\n    \u003C\u002Fdiv>\n    \u003Cbutton type=\"button\" class=\"copy-code-btn flex items-center gap-1.5 px-3 py-1 text-xs font-bold bg-[#2f363d] text-gray-200 border-2 border-black dark:border-gray-500 rounded-lg shadow-neo-sm hover:translate-x-0.5 hover:translate-y-0.5 hover:shadow-none hover:bg-gray-700 transition-all cursor-pointer\" data-code=\"umur%20%3D%20int(baris%5B2%5D)%20%20%20%23%20ValueError%20kalau%20isinya%20bukan%20angka%0Aif%20umur%20%3C%201%20or%20umur%20%3E%20120%3A%0A%20%20%20%20print(%22umur%20tidak%20masuk%20akal%2C%20dilewati%22)\" title=\"Salin kode\">\n      \u003Csvg class=\"w-3.5 h-3.5 copy-icon-svg\" fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\">\n        \u003Cpath stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" d=\"M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z\">\u003C\u002Fpath>\n      \u003C\u002Fsvg>\n      \u003Cspan class=\"copy-text\">Copy\u003C\u002Fspan>\n    \u003C\u002Fbutton>\n  \u003C\u002Fdiv>\n  \u003Cdiv class=\"code-block-content p-4 overflow-x-auto text-sm font-mono leading-relaxed bg-[#24292e]\">\n    \u003Cpre class=\"shiki github-dark\" style=\"background-color:#24292e;color:#e1e4e8\" tabindex=\"0\">\u003Ccode>\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">umur \u003C\u002Fspan>\u003Cspan style=\"color:#F97583\">=\u003C\u002Fspan>\u003Cspan style=\"color:#79B8FF\"> int\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\">(baris[\u003C\u002Fspan>\u003Cspan style=\"color:#79B8FF\">2\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\">])   \u003C\u002Fspan>\u003Cspan style=\"color:#6A737D\"># ValueError kalau isinya bukan angka\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">if\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\"> umur \u003C\u002Fspan>\u003Cspan style=\"color:#F97583\">&#x3C;\u003C\u002Fspan>\u003Cspan style=\"color:#79B8FF\"> 1\u003C\u002Fspan>\u003Cspan style=\"color:#F97583\"> or\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\"> umur \u003C\u002Fspan>\u003Cspan style=\"color:#F97583\">>\u003C\u002Fspan>\u003Cspan style=\"color:#79B8FF\"> 120\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\">:\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#79B8FF\">    print\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\">(\u003C\u002Fspan>\u003Cspan style=\"color:#9ECBFF\">\"umur tidak masuk akal, dilewati\"\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\">)\u003C\u002Fspan>\u003C\u002Fspan>\u003C\u002Fcode>\u003C\u002Fpre>\n  \u003C\u002Fdiv>\n\u003C\u002Fdiv>\u003Cp>Ada dua cacat di dua baris itu. Pertama, \u003Ccode>int()\u003C\u002Fcode> melempar, dan \u003Ccode>main()\u003C\u002Fcode> nggak menangkapnya: satu baris jelek menghentikan seluruh proses, jadi masalah sesudahnya nggak pernah keburu dilaporkan. Kedua, satu-satunya diagnostik cuma \u003Ccode>print\u003C\u002Fcode> yang nggak menyebut baris maupun kolomnya.\u003C\u002Fp>\n\u003Cp>Yang bikin jebakan ini licin: komentar di atasnya justru membenarkan dua-duanya — &quot;Python sudah memberi pesan yang jelas&quot;, &quot;yang bikin sistem rusak cuma umur&quot;. Kodenya \u003Cstrong>kelihatan\u003C\u002Fstrong> hati-hati. Padahal aturan yang kamu butuhkan kebalikannya: \u003Cstrong>satu baris jelek nggak boleh menghentikan seluruh proses.\u003C\u002Fstrong> Baris itu dilaporkan, lalu perulangan jalan terus ke baris berikutnya.\u003C\u002Fp>\n\u003Ch3 id=\"nomor-baris-itu-urusan-yang-memanggil-bukan-yang-memvalidasi\">Nomor baris itu urusan yang memanggil, bukan yang memvalidasi\u003C\u002Fh3>\n\u003Cp>\u003Ccode>periksa()\u003C\u002Fcode> sebaiknya \u003Cstrong>mengembalikan daftar masalah\u003C\u002Fstrong> dan nggak tahu apa-apa soal nomor baris. Yang menempelkan nomornya adalah \u003Ccode>main()\u003C\u002Fcode>, lewat \u003Ccode>enumerate(pembaca, start=2)\u003C\u002Fcode>. Angka \u003Ccode>2\u003C\u002Fcode> itu bukan detail sepele: baris 1 di file adalah judulnya, jadi baris data pertama ada di baris 2.\u003C\u002Fp>\n\u003Cp>Kalau kamu mulai dari \u003Ccode>start=1\u003C\u002Fcode>, angka yang kamu laporkan salah persis satu dari yang dilihat pengguna di editornya. Inilah \u003Cem>off-by-one\u003C\u002Fem> yang paling gampang lolos dari mata sendiri, karena layarnya &quot;kelihatannya benar&quot;. Yang memisahkan keduanya cuma satu hal: nomor baris \u003Cstrong>di file\u003C\u002Fstrong> itu urusan yang memanggil, sedangkan index data itu urusan yang memvalidasi.\u003C\u002Fp>\n\u003Ch3 id=\"indexerror-sama-saja-belum-memvalidasi\">\u003Ccode>IndexError\u003C\u002Fcode> sama saja belum memvalidasi\u003C\u002Fh3>\n\u003Cp>\u003Ccode>kolom.csv\u003C\u002Fcode> punya baris dengan 2 kolom dan ada juga yang 4 kolom, padahal judulnya cuma punya 3. Menyentuh \u003Ccode>baris[2]\u003C\u002Fcode> sebelum memastikan jumlah kolomnya cukup cuma memindahkan lokasi crash-nya. \u003Cstrong>\u003Ccode>IndexError\u003C\u002Fcode> di sini artinya belum memvalidasi\u003C\u002Fstrong> — bukan tanda data yang rumit.\u003C\u002Fp>\n\u003Cp>Urutan yang benar: periksa jumlah kolom lebih dulu. Kalau \u003Ccode>len(baris)\u003C\u002Fcode> nggak sama dengan jumlah kolom yang diharapkan, laporkan sebagai satu masalah di kolom \u003Ccode>jumlah kolom\u003C\u002Fcode>, lalu jangan lanjut memeriksa isi baris itu. Kolom per kolom belum bisa dipercaya sebelum jumlahnya cocok.\u003C\u002Fp>\n\u003Ch3 id=\"kode-keluar-adalah-bagian-dari-kontrak\">Kode keluar adalah bagian dari kontrak\u003C\u002Fh3>\n\u003Cp>Script seperti ini dipanggil dari \u003Ccode>make\u003C\u002Fcode>, \u003Ccode>cron\u003C\u002Fcode>, atau pipa shell. Pemanggilnya nggak membaca layar; dia membaca \u003Cem>exit code\u003C\u002Fem>. &quot;File tidak ada&quot; dan &quot;file ada tapi isinya ada yang salah&quot; itu dua hal yang beda bagi pemanggil — yang pertama artinya salah pakai, yang kedua artinya datanya perlu diperbaiki.\u003C\u002Fp>\n\u003Cp>Karena itu kode keluarnya dibedakan: \u003Ccode>0\u003C\u002Fcode> untuk beres, \u003Ccode>1\u003C\u002Fcode> untuk ada baris bermasalah, \u003Ccode>2\u003C\u002Fcode> untuk cara pakainya yang salah (file nggak ada, argumen kurang). Kondisi awal cuma punya satu kode untuk hampir semua kegagalan, jadi pemanggil otomatis nggak bisa memutuskan harus mengulang, berhenti, atau melanjutkan.\u003C\u002Fp>\n\u003Ch2 id=\"bentuk-solusinya\">Bentuk solusinya\u003C\u002Fh2>\n\u003Cp>Validasi jadi fungsi yang \u003Cstrong>mengumpulkan\u003C\u002Fstrong>, bukan yang melempar:\u003C\u002Fp>\n\n\u003Cdiv class=\"code-block-wrapper my-6 not-prose rounded-xl border-2 border-black dark:border-gray-600 shadow-neo overflow-hidden bg-[#24292e] text-[#e1e4e8] transition-all\">\n  \u003Cdiv class=\"code-block-header flex items-center justify-between px-4 py-2.5 bg-[#1f2428] border-b-2 border-black dark:border-gray-600 font-mono text-xs font-bold text-gray-300 select-none\">\n    \u003Cdiv class=\"flex items-center gap-2\">\n      \u003Cspan class=\"w-3 h-3 rounded-full bg-[#ff5f56] border border-black\u002F40 inline-block\">\u003C\u002Fspan>\n      \u003Cspan class=\"w-3 h-3 rounded-full bg-[#ffbd2e] border border-black\u002F40 inline-block\">\u003C\u002Fspan>\n      \u003Cspan class=\"w-3 h-3 rounded-full bg-[#27c93f] border border-black\u002F40 inline-block\">\u003C\u002Fspan>\n      \u003Cspan class=\"ml-2 font-mono font-bold text-xs uppercase tracking-wider text-gray-300\">PYTHON\u003C\u002Fspan>\n    \u003C\u002Fdiv>\n    \u003Cbutton type=\"button\" class=\"copy-code-btn flex items-center gap-1.5 px-3 py-1 text-xs font-bold bg-[#2f363d] text-gray-200 border-2 border-black dark:border-gray-500 rounded-lg shadow-neo-sm hover:translate-x-0.5 hover:translate-y-0.5 hover:shadow-none hover:bg-gray-700 transition-all cursor-pointer\" data-code=\"def%20periksa(baris)%3A%0A%20%20%20%20masalah%20%3D%20%5B%5D%0A%20%20%20%20if%20len(baris)%20!%3D%20len(KOLOM)%3A%0A%20%20%20%20%20%20%20%20masalah.append((%22jumlah%20kolom%22%2C%20%22...%20seharusnya%20...%22))%0A%20%20%20%20%20%20%20%20return%20masalah%0A%20%20%20%20nama%2C%20email%2C%20umur%20%3D%20(sel.strip()%20for%20sel%20in%20baris)%0A%20%20%20%20if%20not%20nama%3A%0A%20%20%20%20%20%20%20%20masalah.append((%22nama%22%2C%20%22kolom%20nama%20kosong%2C%20padahal%20harus%20diisi%22))%0A%20%20%20%20if%20%22%40%22%20not%20in%20email%3A%0A%20%20%20%20%20%20%20%20masalah.append((%22email%22%2C%20f%22'%7Bemail%7D'%20bukan%20alamat%20email%20karena%20tidak%20ada%20tanda%20'%40'%22))%0A%20%20%20%20try%3A%0A%20%20%20%20%20%20%20%20umur_angka%20%3D%20int(umur)%0A%20%20%20%20except%20ValueError%3A%0A%20%20%20%20%20%20%20%20masalah.append((%22umur%22%2C%20f%22'%7Bumur%7D'%20bukan%20angka%20%E2%80%94%20isi%20dengan%20bilangan%20bulat%2C%20misalnya%2025%22))%0A%20%20%20%20return%20masalah\" title=\"Salin kode\">\n      \u003Csvg class=\"w-3.5 h-3.5 copy-icon-svg\" fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\">\n        \u003Cpath stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" d=\"M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z\">\u003C\u002Fpath>\n      \u003C\u002Fsvg>\n      \u003Cspan class=\"copy-text\">Copy\u003C\u002Fspan>\n    \u003C\u002Fbutton>\n  \u003C\u002Fdiv>\n  \u003Cdiv class=\"code-block-content p-4 overflow-x-auto text-sm font-mono leading-relaxed bg-[#24292e]\">\n    \u003Cpre class=\"shiki github-dark\" style=\"background-color:#24292e;color:#e1e4e8\" tabindex=\"0\">\u003Ccode>\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">def\u003C\u002Fspan>\u003Cspan style=\"color:#B392F0\"> periksa\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\">(baris):\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">    masalah \u003C\u002Fspan>\u003Cspan style=\"color:#F97583\">=\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\"> []\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">    if\u003C\u002Fspan>\u003Cspan style=\"color:#79B8FF\"> len\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\">(baris) \u003C\u002Fspan>\u003Cspan style=\"color:#F97583\">!=\u003C\u002Fspan>\u003Cspan style=\"color:#79B8FF\"> len\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\">(\u003C\u002Fspan>\u003Cspan style=\"color:#79B8FF\">KOLOM\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\">):\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">        masalah.append((\u003C\u002Fspan>\u003Cspan style=\"color:#9ECBFF\">\"jumlah kolom\"\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\">, \u003C\u002Fspan>\u003Cspan style=\"color:#9ECBFF\">\"... seharusnya ...\"\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\">))\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">        return\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\"> masalah\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">    nama, email, umur \u003C\u002Fspan>\u003Cspan style=\"color:#F97583\">=\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\"> (sel.strip() \u003C\u002Fspan>\u003Cspan style=\"color:#F97583\">for\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\"> sel \u003C\u002Fspan>\u003Cspan style=\"color:#F97583\">in\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\"> baris)\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">    if\u003C\u002Fspan>\u003Cspan style=\"color:#F97583\"> not\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\"> nama:\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">        masalah.append((\u003C\u002Fspan>\u003Cspan style=\"color:#9ECBFF\">\"nama\"\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\">, \u003C\u002Fspan>\u003Cspan style=\"color:#9ECBFF\">\"kolom nama kosong, padahal harus diisi\"\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\">))\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">    if\u003C\u002Fspan>\u003Cspan style=\"color:#9ECBFF\"> \"@\"\u003C\u002Fspan>\u003Cspan style=\"color:#F97583\"> not\u003C\u002Fspan>\u003Cspan style=\"color:#F97583\"> in\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\"> email:\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">        masalah.append((\u003C\u002Fspan>\u003Cspan style=\"color:#9ECBFF\">\"email\"\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\">, \u003C\u002Fspan>\u003Cspan style=\"color:#F97583\">f\u003C\u002Fspan>\u003Cspan style=\"color:#9ECBFF\">\"'\u003C\u002Fspan>\u003Cspan style=\"color:#79B8FF\">{\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\">email\u003C\u002Fspan>\u003Cspan style=\"color:#79B8FF\">}\u003C\u002Fspan>\u003Cspan style=\"color:#9ECBFF\">' bukan alamat email karena tidak ada tanda '@'\"\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\">))\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">    try\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\">:\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">        umur_angka \u003C\u002Fspan>\u003Cspan style=\"color:#F97583\">=\u003C\u002Fspan>\u003Cspan style=\"color:#79B8FF\"> int\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\">(umur)\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">    except\u003C\u002Fspan>\u003Cspan style=\"color:#79B8FF\"> ValueError\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\">:\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">        masalah.append((\u003C\u002Fspan>\u003Cspan style=\"color:#9ECBFF\">\"umur\"\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\">, \u003C\u002Fspan>\u003Cspan style=\"color:#F97583\">f\u003C\u002Fspan>\u003Cspan style=\"color:#9ECBFF\">\"'\u003C\u002Fspan>\u003Cspan style=\"color:#79B8FF\">{\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\">umur\u003C\u002Fspan>\u003Cspan style=\"color:#79B8FF\">}\u003C\u002Fspan>\u003Cspan style=\"color:#9ECBFF\">' bukan angka — isi dengan bilangan bulat, misalnya 25\"\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\">))\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">    return\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\"> masalah\u003C\u002Fspan>\u003C\u002Fspan>\u003C\u002Fcode>\u003C\u002Fpre>\n  \u003C\u002Fdiv>\n\u003C\u002Fdiv>\u003Cp>lalu di \u003Ccode>main()\u003C\u002Fcode> nomor barisnya ditempel dari baris file:\u003C\u002Fp>\n\n\u003Cdiv class=\"code-block-wrapper my-6 not-prose rounded-xl border-2 border-black dark:border-gray-600 shadow-neo overflow-hidden bg-[#24292e] text-[#e1e4e8] transition-all\">\n  \u003Cdiv class=\"code-block-header flex items-center justify-between px-4 py-2.5 bg-[#1f2428] border-b-2 border-black dark:border-gray-600 font-mono text-xs font-bold text-gray-300 select-none\">\n    \u003Cdiv class=\"flex items-center gap-2\">\n      \u003Cspan class=\"w-3 h-3 rounded-full bg-[#ff5f56] border border-black\u002F40 inline-block\">\u003C\u002Fspan>\n      \u003Cspan class=\"w-3 h-3 rounded-full bg-[#ffbd2e] border border-black\u002F40 inline-block\">\u003C\u002Fspan>\n      \u003Cspan class=\"w-3 h-3 rounded-full bg-[#27c93f] border border-black\u002F40 inline-block\">\u003C\u002Fspan>\n      \u003Cspan class=\"ml-2 font-mono font-bold text-xs uppercase tracking-wider text-gray-300\">PYTHON\u003C\u002Fspan>\n    \u003C\u002Fdiv>\n    \u003Cbutton type=\"button\" class=\"copy-code-btn flex items-center gap-1.5 px-3 py-1 text-xs font-bold bg-[#2f363d] text-gray-200 border-2 border-black dark:border-gray-500 rounded-lg shadow-neo-sm hover:translate-x-0.5 hover:translate-y-0.5 hover:shadow-none hover:bg-gray-700 transition-all cursor-pointer\" data-code=\"for%20nomor%2C%20baris%20in%20enumerate(pembaca%2C%20start%3D2)%3A%20%20%20%23%20baris%201%20file%20%3D%20judul%0A%20%20%20%20for%20kolom%2C%20pesan%20in%20periksa(baris)%3A%0A%20%20%20%20%20%20%20%20sys.stderr.write(f%22baris%20%7Bnomor%7D%3A%20%7Bkolom%7D%20%E2%80%94%20%7Bpesan%7D%5Cn%22)\" title=\"Salin kode\">\n      \u003Csvg class=\"w-3.5 h-3.5 copy-icon-svg\" fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\">\n        \u003Cpath stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" d=\"M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z\">\u003C\u002Fpath>\n      \u003C\u002Fsvg>\n      \u003Cspan class=\"copy-text\">Copy\u003C\u002Fspan>\n    \u003C\u002Fbutton>\n  \u003C\u002Fdiv>\n  \u003Cdiv class=\"code-block-content p-4 overflow-x-auto text-sm font-mono leading-relaxed bg-[#24292e]\">\n    \u003Cpre class=\"shiki github-dark\" style=\"background-color:#24292e;color:#e1e4e8\" tabindex=\"0\">\u003Ccode>\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">for\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\"> nomor, baris \u003C\u002Fspan>\u003Cspan style=\"color:#F97583\">in\u003C\u002Fspan>\u003Cspan style=\"color:#79B8FF\"> enumerate\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\">(pembaca, \u003C\u002Fspan>\u003Cspan style=\"color:#FFAB70\">start\u003C\u002Fspan>\u003Cspan style=\"color:#F97583\">=\u003C\u002Fspan>\u003Cspan style=\"color:#79B8FF\">2\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\">):   \u003C\u002Fspan>\u003Cspan style=\"color:#6A737D\"># baris 1 file = judul\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">    for\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\"> kolom, pesan \u003C\u002Fspan>\u003Cspan style=\"color:#F97583\">in\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\"> periksa(baris):\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">        sys.stderr.write(\u003C\u002Fspan>\u003Cspan style=\"color:#F97583\">f\u003C\u002Fspan>\u003Cspan style=\"color:#9ECBFF\">\"baris \u003C\u002Fspan>\u003Cspan style=\"color:#79B8FF\">{\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\">nomor\u003C\u002Fspan>\u003Cspan style=\"color:#79B8FF\">}\u003C\u002Fspan>\u003Cspan style=\"color:#9ECBFF\">: \u003C\u002Fspan>\u003Cspan style=\"color:#79B8FF\">{\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\">kolom\u003C\u002Fspan>\u003Cspan style=\"color:#79B8FF\">}\u003C\u002Fspan>\u003Cspan style=\"color:#9ECBFF\"> — \u003C\u002Fspan>\u003Cspan style=\"color:#79B8FF\">{\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\">pesan\u003C\u002Fspan>\u003Cspan style=\"color:#79B8FF\">}\\n\u003C\u002Fspan>\u003Cspan style=\"color:#9ECBFF\">\"\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\">)\u003C\u002Fspan>\u003C\u002Fspan>\u003C\u002Fcode>\u003C\u002Fpre>\n  \u003C\u002Fdiv>\n\u003C\u002Fdiv>\u003Cp>Satu baris yang benar mengembalikan daftar kosong, jadi nggak ada yang dilaporkan. Satu baris yang salah bisa mengembalikan \u003Cstrong>lebih dari satu\u003C\u002Fstrong> masalah sekaligus — di \u003Ccode>beberapa.csv\u003C\u002Fcode> ada satu baris yang nama kosong \u003Cstrong>dan\u003C\u002Fstrong> emailnya tanpa \u003Ccode>@\u003C\u002Fcode>. Perhatikan juga bahwa \u003Ccode>try\u002Fexcept\u003C\u002Fcode>-nya kecil dan spesifik, cuma membungkus \u003Ccode>int(umur)\u003C\u002Fcode>. Jangan bungkus semuanya dengan satu \u003Ccode>try\u002Fexcept\u003C\u002Fcode> besar di \u003Ccode>main()\u003C\u002Fcode>: itu memang menghilangkan \u003Cem>stack trace\u003C\u002Fem>, tapi sekalian menghentikan program di baris jelek pertama, dan pekerjaanmu balik ke titik awal.\u003C\u002Fp>\n\u003Ch2 id=\"yang-perlu-kamu-bawa-dari-sini\">Yang perlu kamu bawa dari sini\u003C\u002Fh2>\n\u003Cp>Pesan error yang berguna menjawab tiga hal — \u003Cstrong>di mana\u003C\u002Fstrong>, \u003Cstrong>apa yang salah\u003C\u002Fstrong>, dan \u003Cstrong>harusnya bagaimana\u003C\u002Fstrong> — dan jawabannya untuk semuanya sekaligus, bukan cuma yang pertama kebetulan ditemui. Program yang berhenti di masalah pertama cuma menjawab sepertiganya. Sisa waktu yang kamu hemat di situ, kamu bayar mahal di telepon yang berdering.\u003C\u002Fp>\n\u003Cp>Satu kebiasaan saja yang berubah: sebelum menulis \u003Ccode>raise\u003C\u002Fcode>, tanya dulu siapa yang akan membaca pesannya.\u003C\u002Fp>\n","\u003Cul>\n\u003Cli>\u003Cstrong>Satu baris jelek nggak boleh menghentikan seluruh proses.\u003C\u002Fstrong> Laporkan, lalu lanjut ke baris berikutnya.\u003C\u002Fli>\n\u003Cli>\u003Cstrong>Nomor baris di file dimulai dari \u003Ccode>enumerate(pembaca, start=2)\u003C\u002Fcode>\u003C\u002Fstrong> — baris 1 adalah judulnya, bukan data.\u003C\u002Fli>\n\u003Cli>\u003Cstrong>\u003Ccode>IndexError\u003C\u002Fcode> itu artinya belum memvalidasi.\u003C\u002Fstrong> Periksa jumlah kolom dulu sebelum menyentuh isi baris.\u003C\u002Fli>\n\u003Cli>\u003Cstrong>Kode keluar itu kontrak:\u003C\u002Fstrong> \u003Ccode>0\u003C\u002Fcode> beres, \u003Ccode>1\u003C\u002Fcode> ada baris bermasalah, \u003Ccode>2\u003C\u002Fcode> cara pakainya salah.\u003C\u002Fli>\n\u003C\u002Ful>\n",[185,203,216,229,248,266],{"id":186,"title":187,"title_en":8,"slug":188,"excerpt":189,"excerpt_en":8,"has_translation":11,"published_at":190,"reading_time":191,"hidden_from_listing":11,"tags":192,"thumbnail_image":8,"og_image":194,"series":195,"authors":200},132,"Python Tuples: Why All Three Points Are the Same Point","python-tuples","Three points walk into a loop and come out identical. The list was never copied three times; the name was. A tuple makes that bug impossible.","2026-10-28T12:00:00.000Z",6,[81,193],"tuple","\u002Fog\u002Fpython-tuples.png",{"id":196,"title":197,"title_en":198,"slug":199},30,"Koleksi dan String","Collections and Strings","python-collections",[201],{"authors_id":202},{"id":107,"name":108,"slug":109,"role":110,"profile_picture":8},{"id":204,"title":205,"title_en":8,"slug":206,"excerpt":207,"excerpt_en":8,"has_translation":11,"published_at":208,"reading_time":191,"hidden_from_listing":11,"tags":209,"thumbnail_image":8,"og_image":211,"series":212,"authors":213},131,"Python Sets: the Duplicate That Keeps Coming Back","python-sets","A de-duplication loop that works on clean data leaves four tags where there should be two. The bug is case, and the fix is a `set`.","2026-10-28T11:00:00.000Z",[81,210],"set","\u002Fog\u002Fpython-sets.png",{"id":196,"title":197,"title_en":198,"slug":199},[214],{"authors_id":215},{"id":107,"name":108,"slug":109,"role":110,"profile_picture":8},{"id":217,"title":218,"title_en":8,"slug":219,"excerpt":220,"excerpt_en":8,"has_translation":11,"published_at":221,"reading_time":191,"hidden_from_listing":11,"tags":222,"thumbnail_image":8,"og_image":224,"series":225,"authors":226},130,"Python Dictionaries: the Key That Isn't There","python-dictionary","A word counter dies the first time it meets a word twice. The fix is one line; what takes longer is knowing which of a dict's three lookups to reach for.","2026-10-28T10:00:00.000Z",[81,223],"dictionary","\u002Fog\u002Fpython-dictionary.png",{"id":196,"title":197,"title_en":198,"slug":199},[227],{"authors_id":228},{"id":107,"name":108,"slug":109,"role":110,"profile_picture":8},{"id":230,"title":231,"title_en":8,"slug":232,"excerpt":233,"excerpt_en":8,"has_translation":11,"published_at":234,"reading_time":13,"hidden_from_listing":11,"tags":235,"thumbnail_image":8,"og_image":242,"series":243,"authors":245},129,"Biar RAG nggak kena 504: pisahin jalur query dan ingestion","rag-pisah-jalur-query-dan-ingestion","Satu service RAG yang merangkap parsing dan menjawab kueri bakal tumbang pas ada yang upload PDF 200 halaman. Obatnya dua jalur dan satu queue.","2026-10-22T01:30:00.000Z",[236,237,238,239,240,241],"rag","cloud-run","cloud-tasks","gcp","serverless","arsitektur","\u002Fog\u002Frag-pisah-jalur-query-dan-ingestion.png",{"id":244,"title":32,"title_en":33,"slug":34},8,[246],{"authors_id":247},{"id":107,"name":108,"slug":109,"role":110,"profile_picture":8},{"id":249,"title":250,"title_en":8,"slug":251,"excerpt":252,"excerpt_en":8,"has_translation":11,"published_at":253,"reading_time":191,"hidden_from_listing":11,"tags":254,"thumbnail_image":8,"og_image":261,"series":262,"authors":263},128,"Linear atau graf: cara mengatur banyak agent tanpa loop abadi","multi-agen-linear-atau-grafik","Satu agent dengan dua puluh tool gampang kacau. Begitu beberapa agent saling revisi, kamu butuh graf yang mendukung cycle dan angka yang membuatnya berhenti.","2026-10-21T16:05:00.000Z",[255,256,257,258,259,260],"multi-agent","langgraph","autogen","orkestrasi","checkpoint","llm","\u002Fog\u002Fmulti-agen-linear-atau-grafik.png",{"id":244,"title":32,"title_en":33,"slug":34},[264],{"authors_id":265},{"id":107,"name":108,"slug":109,"role":110,"profile_picture":8},{"id":267,"title":268,"title_en":8,"slug":269,"excerpt":270,"excerpt_en":8,"has_translation":11,"published_at":271,"reading_time":191,"hidden_from_listing":11,"tags":272,"thumbnail_image":8,"og_image":278,"series":279,"authors":280},127,"Kenapa RAG yang cuma andalkan cosine similarity sering nyasar","kenapa-rag-cuma-cosine-similarity-sering-nyasar","RAG yang cuma andalkan embedding balikin chunk yang mirip maknanya, bukan yang kamu butuh. Ini penyebabnya, dan dua lapis penambalnya.","2026-10-19T02:04:06.718Z",[236,273,274,275,276,277],"vector-search","bm25","hybrid-search","retrieval","embedding","\u002Fog\u002Fkenapa-rag-cuma-cosine-similarity-sering-nyasar.png",{"id":244,"title":32,"title_en":33,"slug":34},[281],{"authors_id":282},{"id":107,"name":108,"slug":109,"role":110,"profile_picture":8},[284,288,291,294,297,300,303,306],{"text":285,"depth":286,"id":287},"Angka lengkapnya",2,"angka-lengkapnya",{"text":289,"depth":286,"id":290},"Kenapa kondisinya begini","kenapa-kondisinya-begini",{"text":292,"depth":136,"id":293},"periksa() mencetak, bukan mengembalikan — dan melempar di tengah jalan","periksa-mencetak-bukan-mengembalikan-dan-melempar-di-tengah-jalan",{"text":295,"depth":136,"id":296},"Nomor baris itu urusan yang memanggil, bukan yang memvalidasi","nomor-baris-itu-urusan-yang-memanggil-bukan-yang-memvalidasi",{"text":298,"depth":136,"id":299},"IndexError sama saja belum memvalidasi","indexerror-sama-saja-belum-memvalidasi",{"text":301,"depth":136,"id":302},"Kode keluar adalah bagian dari kontrak","kode-keluar-adalah-bagian-dari-kontrak",{"text":304,"depth":286,"id":305},"Bentuk solusinya","bentuk-solusinya",{"text":307,"depth":286,"id":308},"Yang perlu kamu bawa dari sini","yang-perlu-kamu-bawa-dari-sini",1793246466983]