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:

python
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):

text
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:

  1. Baris + Exception Group Traceback ...: tanda + dan | di kolom kiri adalah penanda bahwa yang naik adalah grup, bukan exception tunggal.
  2. Baris ExceptionGroup: dua sumber gagal (2 sub-exceptions): pesan grup beserta jumlah anaknya.
  3. Blok +-+---------------- 1 ---------------- hingga 2: setiap anak exception ditampilkan bernomor dengan tanda bintang-pembatas; keduanya naik bersamaan dalam satu lemparan.
  4. Baris File "demo_grup.py", line 2: frame panggilan menunjuk titik raise yang 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:

python
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:

text
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:

python
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:

text
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.

SkenarioAlat yang tepat
Satu operasi, satu kemungkinan galatraise ValueError(...) biasa
Validasi form dengan banyak field gagal bersamaanExceptionGroup + except*
Beberapa task async gagal bersamaanasyncio.TaskGroup (membentuk grup otomatis)
Menangani subset galat konkuren secara berbedaexcept* TipeA dan except* TipeB berdampingan