Mekanisme try/except klasik dibangun dengan asumsi satu titik kegagalan: satu exception naik, satu handler menangkap. Asumsi itu runtuh di dunia konkuren — ketika sepuluh task berjalan bersamaan dan lima di antaranya gagal, exception mana yang “yang” naik? Python 3.11 menjawab dengan exception groups (PEP 654): satu wadah bernama ExceptionGroup yang membawa kumpulan exception, dan klausa except* untuk menangkap bagiannya. Materi dasar menangani beberapa exception telah dibahas pada Chapter Menangani Beberapa Exceptions; Chapter ini fokus pada mekanisme grup yang baru.
Membuat dan Menaikkan ExceptionGroup #
Class ExceptionGroup menerima dua argumen: pesan dan iterable berisi exception anak. Kita bisa menaikkannya seperti exception biasa. Simpan sebagai demo_grup.py, lalu jalankan python3 demo_grup.py:
def muat_data():
raise ExceptionGroup("dua sumber gagal", [
ConnectionError("API utama tidak merespons"),
TimeoutError("API cadangan kehabisan waktu"),
])
print("memuat data...")
muat_data()Output yang Dihasilkan (dicetak interpreter Python ke stderr):
memuat data...
+ Exception Group Traceback (most recent call last):
| File "demo_grup.py", line 9, in <module>
| muat_data()
| File "demo_grup.py", line 2, in muat_data
| raise ExceptionGroup("dua sumber gagal", [
| ExceptionGroup: dua sumber gagal (2 sub-exceptions)
+-+---------------- 1 ----------------
| ConnectionError: API utama tidak merespons
+---------------- 2 ----------------
| TimeoutError: API cadangan kehabisan waktu
+------------------------------------Baca struktur traceback grup tersebut:
- Baris
+ Exception Group Traceback ...: tanda+dan|di kolom kiri adalah penanda bahwa yang naik adalah grup, bukan exception tunggal. - Baris
ExceptionGroup: dua sumber gagal (2 sub-exceptions): pesan grup beserta jumlah anaknya. - Blok
+-+---------------- 1 ----------------hingga2: setiap anak exception ditampilkan bernomor dengan tanda bintang-pembatas; keduanya naik bersamaan dalam satu lemparan. - Baris
File "demo_grup.py", line 2: frame panggilan menunjuk titikraiseyang sama — satu raise, banyak galat.
Sebagai pesan error, format ini jujur: pembaca melihat seluruh kegagalan sekaligus, bukan yang pertama saja lalu sisanya tertelan. Perlu dicatat bahwa ExceptionGroup hanya menerima anak yang mewarisi Exception; menaikkan BaseExceptionGroup diperlukan untuk membawa KeyboardInterrupt atau SystemExit.
Klausa except* untuk Menangkap Per Tipe #
Klausa except biasa menangkap grup sebagai satu kesatuan — kurang berguna bila kita ingin menangani ValueError dengan cara berbeda dari KeyError. Klausa except* (dibaca “except star”) memecah grup: ia mengambil hanya anak yang cocok dengan tipenya dan membungkusnya dalam grup baru:
def jalankan_pemeriksaan():
raise ExceptionGroup(
"pemeriksaan data gagal",
[
ValueError("stok tidak boleh negatif"),
KeyError("kolom 'harga' hilang"),
],
)
try:
jalankan_pemeriksaan()
except* ValueError as eg:
print("Menangani ValueError:", len(eg.exceptions), "galat")
for galat in eg.exceptions:
print(" -", galat)
except* KeyError as eg:
print("Menangani KeyError:", len(eg.exceptions), "galat")
for galat in eg.exceptions:
print(" -", galat)Output yang Dihasilkan:
Menangani ValueError: 1 galat
- stok tidak boleh negatif
Menangani KeyError: 1 galat
- "kolom 'harga' hilang"Dua hal terjadi di balik layar. Pertama, interpreter Python memeriksa setiap anak grup terhadap semua klausa except* — keduanya berjalan, bukan saling meniadakan seperti except biasa. Kedua, variabel eg pada tiap klausa adalah ExceptionGroup baru yang hanya berisi anak yang cocok; atribut eg.exceptions adalah tuple berisi anak tersebut.
Bagaimana bila hanya sebagian tipe yang ditangani? Anak yang tidak terdapat handler-nya naik kembali sebagai grup sisa:
try:
try:
raise ExceptionGroup("ganda", [ValueError("data korup"), KeyError("kolom hilang")])
except* ValueError as eg:
print("Tertangani:", eg.exceptions)
except ExceptionGroup as sisa:
print("Lolos :", [type(e).__name__ for e in sisa.exceptions])Output yang Dihasilkan:
Tertangani: (ValueError('data korup'),)
Lolos : ['KeyError']Pada contoh dua lapis di atas, klausa dalam hanya menangani ValueError; interpreter Python membungkus KeyError yang tersisa ke dalam grup baru dan menaikkannya, lalu klausa luar (grup ditangkap except biasa karena kita tahu sisanya satu kesatuan) melaporkan isinya. Perhatikan juga isi except* ValueError: penulisan as eg memberi grup, bukan exception telanjang — itulah sebabnya eg.exceptions selalu berbentuk tuple.
Kapan Grup Dibutuhkan dan Kapan Tidak #
Exception group bukan pengganti try/except biasa. Gunakan grup dan except* hanya ketika banyak kegagalan benar-benar bisa terjadi sekaligus — skenario utamanya adalah konkurensi. Sumber pemakaian terbesar di stdlib adalah asyncio.TaskGroup, yang otomatis membungkus kegagalan task-task-nya menjadi satu grup; kerja sama ini dibahas tuntas pada Chapter Task Groups.
| Skenario | Alat yang tepat |
|---|---|
| Satu operasi, satu kemungkinan galat | raise ValueError(...) biasa |
| Validasi form dengan banyak field gagal bersamaan | ExceptionGroup + except* |
| Beberapa task async gagal bersamaan | asyncio.TaskGroup (membentuk grup otomatis) |
| Menangani subset galat konkuren secara berbeda | except* TipeA dan except* TipeB berdampingan |