8. Kesalahan *errors* dan Pengecualian *exceptions*
***************************************************

Sampai sekarang pesan kesalahan belum lebih dari yang disebutkan,
tetapi jika Anda telah mencoba contohnya, Anda mungkin telah melihat
beberapa. Ada (setidaknya) dua jenis kesalahan yang dapat dibedakan:
*syntax errors* dan *exceptions*.


8.1. Kesalahan Sintaksis
========================

Kesalahan sintaksis, juga dikenal sebagai kesalahan penguraian
*parsing*, mungkin merupakan jenis keluhan paling umum yang Anda
dapatkan saat Anda masih belajar Python:

   >>> while True print('Hello world')
     File "<stdin>", line 1
       while True print('Hello world')
                  ^^^^^
   SyntaxError: invalid syntax

The parser repeats the offending line and displays little 'arrow's
pointing at the token in the line where the error was detected.  The
error may be caused by the absence of a token *before* the indicated
token.  In the example, the error is detected at the function
"print()", since a colon ("':'") is missing before it.  File name and
line number are printed so you know where to look in case the input
came from a script.


8.2. Pengecualian
=================

Bahkan jika suatu pernyataan atau ungkapan secara sintaksis benar, itu
dapat menyebabkan kesalahan ketika suatu usaha dilakukan untuk
mengeksekusinya. Kesalahan yang terdeteksi selama eksekusi disebut
*exceptions* dan tidak fatal tanpa syarat: Anda akan segera belajar
cara menanganinya dalam program Python. Namun, sebagian besar
pengecualian tidak ditangani oleh program, dan menghasilkan pesan
kesalahan seperti yang ditunjukkan di sini:

   >>> 10 * (1/0)
   Traceback (most recent call last):
     File "<stdin>", line 1, in <module>
       10 * (1/0)
             ~^~
   ZeroDivisionError: division by zero
   >>> 4 + spam*3
   Traceback (most recent call last):
     File "<stdin>", line 1, in <module>
       4 + spam*3
           ^^^^
   NameError: name 'spam' is not defined
   >>> '2' + 2
   Traceback (most recent call last):
     File "<stdin>", line 1, in <module>
       '2' + 2
       ~~~~^~~
   TypeError: can only concatenate str (not "int") to str

Baris terakhir dari pesan kesalahan menunjukkan apa yang terjadi.
Pengecualian ada berbagai jenis yang berbeda, dan tipe dicetak sebagai
bagian dari pesan: tipe dalam contoh adalah "ZeroDivisionError",
"NameError" dan "TypeError". String yang dicetak sebagai jenis
pengecualian adalah nama pengecualian bawaan yang terjadi. Ini berlaku
untuk semua pengecualian bawaan, tetapi tidak harus sama untuk
pengecualian yang dibuat pengguna (meskipun ini adalah konvensi yang
bermanfaat). Nama pengecualian standar adalah pengidentifikasi bawaan
(bukan kata kunci yang dipesan *reserved keyword*).

Sisa baris menyediakan detail berdasarkan jenis pengecualian dan apa
yang menyebabkannya.

The preceding part of the error message shows the context where the
exception occurred, in the form of a stack traceback. In general it
contains a stack traceback listing source lines; however, it will not
display lines read from standard input.

Built-in Exceptions memberikan daftar pengecualian bawaan dan artinya.


8.3. Menangani Pengecualian
===========================

Dimungkinkan untuk menulis program yang menangani pengecualian yang
dipilih. Lihatlah contoh berikut, yang meminta masukan dari pengguna
sampai integer yang valid telah dimasukkan, tetapi memungkinkan
pengguna untuk menghentikan program (menggunakan "Control-C" atau apa
pun yang didukung sistem operasi); perhatikan bahwa gangguan yang
dibuat pengguna ditandai dengan munculnya pengecualian
"KeyboardInterrupt".

   >>> while True:
   ...     try:
   ...         x = int(input("Please enter a number: "))
   ...         break
   ...     except ValueError:
   ...         print("Oops!  That was no valid number.  Try again...")
   ...

Pernyataan "try" berfungsi sebagai berikut.

* Pertama, *try clause* (pernyataan(-pernyataan) di antara kata kunci
  "try" dan "except") dieksekusi.

* Jika tidak ada pengecualian terjadi, *except clause* dilewati dan
  eksekusi pernyataan :keyword: *try* selesai.

* If an exception occurs during execution of the "try" clause, the
  rest of the clause is skipped.  Then, if its type matches the
  exception named after the "except" keyword, the *except clause* is
  executed, and then execution continues after the try/except block.

* If an exception occurs which does not match the exception named in
  the *except clause*, it is passed on to outer "try" statements; if
  no handler is found, it is an *unhandled exception* and execution
  stops with an error message.

A "try" statement may have more than one *except clause*, to specify
handlers for different exceptions.  At most one handler will be
executed. Handlers only handle exceptions that occur in the
corresponding *try clause*, not in other handlers of the same "try"
statement.  An *except clause* may name multiple exceptions as a
parenthesized tuple, for example:

   ... except (RuntimeError, TypeError, NameError):
   ...     pass

A class in an "except" clause matches exceptions which are instances
of the class itself or one of its derived classes (but not the other
way around --- an *except clause* listing a derived class does not
match instances of its base classes). For example, the following code
will print B, C, D in that order:

   class B(Exception):
       pass

   class C(B):
       pass

   class D(C):
       pass

   for cls in [B, C, D]:
       try:
           raise cls()
       except D:
           print("D")
       except C:
           print("C")
       except B:
           print("B")

Note that if the *except clauses* were reversed (with "except B"
first), it would have printed B, B, B --- the first matching *except
clause* is triggered.

When an exception occurs, it may have associated values, also known as
the exception's *arguments*. The presence and types of the arguments
depend on the exception type.

The *except clause* may specify a variable after the exception name.
The variable is bound to the exception instance which typically has an
"args" attribute that stores the arguments. For convenience, builtin
exception types define "__str__()" to print all the arguments without
explicitly accessing ".args".

   >>> try:
   ...     raise Exception('spam', 'eggs')
   ... except Exception as inst:
   ...     print(type(inst))    # the exception type
   ...     print(inst.args)     # arguments stored in .args
   ...     print(inst)          # __str__ allows args to be printed directly,
   ...                          # but may be overridden in exception subclasses
   ...     x, y = inst.args     # unpack args
   ...     print('x =', x)
   ...     print('y =', y)
   ...
   <class 'Exception'>
   ('spam', 'eggs')
   ('spam', 'eggs')
   x = spam
   y = eggs

The exception's "__str__()" output is printed as the last part
('detail') of the message for unhandled exceptions.

"BaseException" is the common base class of all exceptions. One of its
subclasses, "Exception", is the base class of all the non-fatal
exceptions. Exceptions which are not subclasses of "Exception" are not
typically handled, because they are used to indicate that the program
should terminate. They include "SystemExit" which is raised by
"sys.exit()" and "KeyboardInterrupt" which is raised when a user
wishes to interrupt the program.

"Exception" can be used as a wildcard that catches (almost)
everything. However, it is good practice to be as specific as possible
with the types of exceptions that we intend to handle, and to allow
any unexpected exceptions to propagate on.

The most common pattern for handling "Exception" is to print or log
the exception and then re-raise it (allowing a caller to handle the
exception as well):

   import sys

   try:
       f = open('myfile.txt')
       s = f.readline()
       i = int(s.strip())
   except OSError as err:
       print("OS error:", err)
   except ValueError:
       print("Could not convert data to an integer.")
   except Exception as err:
       print(f"Unexpected {err=}, {type(err)=}")
       raise

The "try" ... "except" statement has an optional *else clause*, which,
when present, must follow all *except clauses*.  It is useful for code
that must be executed if the *try clause* does not raise an exception.
For example:

   for arg in sys.argv[1:]:
       try:
           f = open(arg, 'r')
       except OSError:
           print('cannot open', arg)
       else:
           print(arg, 'has', len(f.readlines()), 'lines')
           f.close()

Penggunaan klausa "else" lebih baik daripada menambahkan kode tambahan
ke klausa "try" karena menghindari secara tidak sengaja menangkap
pengecualian yang tidak dimunculkan oleh kode yang dilindungi oleh
pernyataan "try" ... :keyword: *!except*.

Exception handlers do not handle only exceptions that occur
immediately in the *try clause*, but also those that occur inside
functions that are called (even indirectly) in the *try clause*. For
example:

   >>> def this_fails():
   ...     x = 1/0
   ...
   >>> try:
   ...     this_fails()
   ... except ZeroDivisionError as err:
   ...     print('Handling run-time error:', err)
   ...
   Handling run-time error: division by zero


8.4. Memunculkan Pengecualian
=============================

Pernyataan "raise" memungkinkan programmer untuk memaksa pengecualian
yang ditentukan terjadi. Sebagai contoh:

   >>> raise NameError('HiThere')
   Traceback (most recent call last):
     File "<stdin>", line 1, in <module>
       raise NameError('HiThere')
   NameError: HiThere

The sole argument to "raise" indicates the exception to be raised.
This must be either an exception instance or an exception class (a
class that derives from "BaseException", such as "Exception" or one of
its subclasses).  If an exception class is passed, it will be
implicitly instantiated by calling its constructor with no arguments:

   raise ValueError  # shorthand for 'raise ValueError()'

Jika Anda perlu menentukan apakah pengecualian muncul tetapi tidak
bermaksud menanganinya, bentuk yang lebih sederhana dari pernyataan
"raise" memungkinkan Anda untuk memunculkan kembali pengecualian:

   >>> try:
   ...     raise NameError('HiThere')
   ... except NameError:
   ...     print('An exception flew by!')
   ...     raise
   ...
   An exception flew by!
   Traceback (most recent call last):
     File "<stdin>", line 2, in <module>
       raise NameError('HiThere')
   NameError: HiThere


8.5. Exception Chaining
=======================

If an unhandled exception occurs inside an "except" section, it will
have the exception being handled attached to it and included in the
error message:

   >>> try:
   ...     open("database.sqlite")
   ... except OSError:
   ...     raise RuntimeError("unable to handle error")
   ...
   Traceback (most recent call last):
     File "<stdin>", line 2, in <module>
       open("database.sqlite")
       ~~~~^^^^^^^^^^^^^^^^^^^
   FileNotFoundError: [Errno 2] No such file or directory: 'database.sqlite'

   During handling of the above exception, another exception occurred:

   Traceback (most recent call last):
     File "<stdin>", line 4, in <module>
       raise RuntimeError("unable to handle error")
   RuntimeError: unable to handle error

To indicate that an exception is a direct consequence of another, the
"raise" statement allows an optional "from" clause:

   # exc must be exception instance or None.
   raise RuntimeError from exc

This can be useful when you are transforming exceptions. For example:

   >>> def func():
   ...     raise ConnectionError
   ...
   >>> try:
   ...     func()
   ... except ConnectionError as exc:
   ...     raise RuntimeError('Failed to open database') from exc
   ...
   Traceback (most recent call last):
     File "<stdin>", line 2, in <module>
       func()
       ~~~~^^
     File "<stdin>", line 2, in func
   ConnectionError

   The above exception was the direct cause of the following exception:

   Traceback (most recent call last):
     File "<stdin>", line 4, in <module>
       raise RuntimeError('Failed to open database') from exc
   RuntimeError: Failed to open database

It also allows disabling automatic exception chaining using the "from
None" idiom:

   >>> try:
   ...     open('database.sqlite')
   ... except OSError:
   ...     raise RuntimeError from None
   ...
   Traceback (most recent call last):
     File "<stdin>", line 4, in <module>
       raise RuntimeError from None
   RuntimeError

For more information about chaining mechanics, see Built-in
Exceptions.


8.6. Pengecualian yang Ditentukan Pengguna
==========================================

Program dapat memberi nama pengecualian mereka sendiri dengan membuat
kelas pengecualian baru (lihat *tut-class* untuk informasi lebih
lanjut tentang kelas Python). Pengecualian biasanya berasal dari kelas
"Exception", baik secara langsung atau tidak langsung.

Exception classes can be defined which do anything any other class can
do, but are usually kept simple, often only offering a number of
attributes that allow information about the error to be extracted by
handlers for the exception.

Sebagian besar pengecualian didefinisikan dengan nama yang diakhiri
dengan "Error", mirip dengan penamaan pengecualian standar.

Many standard modules define their own exceptions to report errors
that may occur in functions they define.


8.7. Mendefinisikan Tindakan Pembersihan
========================================

Pernyataan "try" memiliki klausa opsional lain yang dimaksudkan untuk
menentukan tindakan pembersihan yang harus dijalankan dalam semua
keadaan. Sebagai contoh:

   >>> try:
   ...     raise KeyboardInterrupt
   ... finally:
   ...     print('Goodbye, world!')
   ...
   Goodbye, world!
   Traceback (most recent call last):
     File "<stdin>", line 2, in <module>
       raise KeyboardInterrupt
   KeyboardInterrupt

Jika ada klausa "finally", klausa untuk "finally" akan dijalankan
sebagai tugas terakhir sebelum pernyataan untuk "try" selesai. Klausa
untuk "finally" dapat berjalan bai atau tidak apabila pernyataan "try"
menghasilkan suatu pengecualian. Poin-poin berikut membahas kasus yang
lebih kompleks saat pengecualian terjadi:

* Jika pengecualian terjadi selama eksekusi klausa untuk :keyword:
  *!try*, maka pengecualian tersebut dapat ditangani oleh klausa
  "except". Jika pengecualian tidak ditangani oleh klausa :keyword:
  *!except*, maka pengecualian dimunculkan kembali setelah klausa
  "finally" dieksekusi.

* Pengecualian dapat terjadi selama pelaksanaan klausa "except" atau
  "else". Sekali lagi, pengecualian akan muncul kembali setelah klausa
  "finally" telah dieksekusi.

* If the "finally" clause executes a "break", "continue" or "return"
  statement, exceptions are not re-raised.

* Jika pernyataan klausa untuk "try" mencapai klausa "break",
  "continue" atau :keyword:` return` maka, pernyataan untuk klausa
  "finally" akan dieksekusi sebelum  "break", "continue" atau "return"
  dieksekusi.

* Jika klausa untuk :keyword:!finally`  telah menyertakan pernyataan
  "return", nilai yang dikembalikan akan menjadi salah satu dari
  pernyataan untuk "finally" dan dari klausa         "return", bukan
  nilai dari "try" pernayataan untuk "return".

Sebagai contoh:

   >>> def bool_return():
   ...     try:
   ...         return True
   ...     finally:
   ...         return False
   ...
   >>> bool_return()
   False

Contoh yang lebih rumit:

   >>> def divide(x, y):
   ...     try:
   ...         result = x / y
   ...     except ZeroDivisionError:
   ...         print("division by zero!")
   ...     else:
   ...         print("result is", result)
   ...     finally:
   ...         print("executing finally clause")
   ...
   >>> divide(2, 1)
   result is 2.0
   executing finally clause
   >>> divide(2, 0)
   division by zero!
   executing finally clause
   >>> divide("2", "1")
   executing finally clause
   Traceback (most recent call last):
     File "<stdin>", line 1, in <module>
       divide("2", "0")
       ~~~~~~^^^^^^^^^^
     File "<stdin>", line 3, in divide
       result = x / y
                ~~^~~
   TypeError: unsupported operand type(s) for /: 'str' and 'str'

Seperti yang Anda lihat, klausa "finally" dieksekusi dalam peristiwa
apa pun. "TypeError" yang ditimbulkan dengan membagi dua string tidak
ditangani oleh klausa "except" dan karenanya kembali muncul setelah
klausa "finally" telah dieksekusi.

Dalam aplikasi dunia nyata, klausa "finally" berguna untuk melepaskan
sumber daya eksternal (seperti berkas atau koneksi jaringan), terlepas
dari apakah penggunaan sumber daya tersebut berhasil.


8.8. Tindakan Pembersihan yang Sudah Ditentukan
===============================================

Beberapa objek mendefinisikan tindakan pembersihan standar yang harus
dilakukan ketika objek tidak lagi diperlukan, terlepas dari apakah
operasi menggunakan objek berhasil atau gagal. Lihatlah contoh
berikut, yang mencoba membuka berkas dan mencetak isinya ke layar.

   for line in open("myfile.txt"):
       print(line, end="")

Masalah dengan kode ini adalah bahwa ia membiarkan berkas terbuka
untuk jumlah waktu yang tidak ditentukan setelah bagian kode ini
selesai dieksekusi. Ini bukan masalah dalam skrip sederhana, tetapi
bisa menjadi masalah untuk aplikasi yang lebih besar. Pernyataan
"with" memungkinkan objek seperti berkas digunakan dengan cara yang
memastikan mereka selalu dibersihkan secepatnya dan dengan benar.

   with open("myfile.txt") as f:
       for line in f:
           print(line, end="")

Setelah pernyataan dieksekusi, file *f* selalu ditutup, bahkan jika
ada masalah saat pemrosesan baris-baris. Objek yang, seperti berkas-
berkas, memberikan tindakan pembersihan yang telah ditentukan, akan
menunjukkan ini dalam dokumentasinya.


8.9. Raising and Handling Multiple Unrelated Exceptions
=======================================================

There are situations where it is necessary to report several
exceptions that have occurred. This is often the case in concurrency
frameworks, when several tasks may have failed in parallel, but there
are also other use cases where it is desirable to continue execution
and collect multiple errors rather than raise the first exception.

The builtin "ExceptionGroup" wraps a list of exception instances so
that they can be raised together. It is an exception itself, so it can
be caught like any other exception.

   >>> def f():
   ...     excs = [OSError('error 1'), SystemError('error 2')]
   ...     raise ExceptionGroup('there were problems', excs)
   ...
   >>> f()
     + Exception Group Traceback (most recent call last):
     |   File "<stdin>", line 1, in <module>
     |     f()
     |     ~^^
     |   File "<stdin>", line 3, in f
     |     raise ExceptionGroup('there were problems', excs)
     | ExceptionGroup: there were problems (2 sub-exceptions)
     +-+---------------- 1 ----------------
       | OSError: error 1
       +---------------- 2 ----------------
       | SystemError: error 2
       +------------------------------------
   >>> try:
   ...     f()
   ... except Exception as e:
   ...     print(f'caught {type(e)}: e')
   ...
   caught <class 'ExceptionGroup'>: e
   >>>

By using "except*" instead of "except", we can selectively handle only
the exceptions in the group that match a certain type. In the
following example, which shows a nested exception group, each
"except*" clause extracts from the group exceptions of a certain type
while letting all other exceptions propagate to other clauses and
eventually to be reraised.

   >>> def f():
   ...     raise ExceptionGroup(
   ...         "group1",
   ...         [
   ...             OSError(1),
   ...             SystemError(2),
   ...             ExceptionGroup(
   ...                 "group2",
   ...                 [
   ...                     OSError(3),
   ...                     RecursionError(4)
   ...                 ]
   ...             )
   ...         ]
   ...     )
   ...
   >>> try:
   ...     f()
   ... except* OSError as e:
   ...     print("There were OSErrors")
   ... except* SystemError as e:
   ...     print("There were SystemErrors")
   ...
   There were OSErrors
   There were SystemErrors
     + Exception Group Traceback (most recent call last):
     |   File "<stdin>", line 2, in <module>
     |     f()
     |     ~^^
     |   File "<stdin>", line 2, in f
     |     raise ExceptionGroup(
     |     ...<12 lines>...
     |     )
     | ExceptionGroup: group1 (1 sub-exception)
     +-+---------------- 1 ----------------
       | ExceptionGroup: group2 (1 sub-exception)
       +-+---------------- 1 ----------------
         | RecursionError: 4
         +------------------------------------
   >>>

Note that the exceptions nested in an exception group must be
instances, not types. This is because in practice the exceptions would
typically be ones that have already been raised and caught by the
program, along the following pattern:

   >>> excs = []
   ... for test in tests:
   ...     try:
   ...         test.run()
   ...     except Exception as e:
   ...         excs.append(e)
   ...
   >>> if excs:
   ...    raise ExceptionGroup("Test Failures", excs)
   ...


8.10. Enriching Exceptions with Notes
=====================================

When an exception is created in order to be raised, it is usually
initialized with information that describes the error that has
occurred. There are cases where it is useful to add information after
the exception was caught. For this purpose, exceptions have a method
"add_note(note)" that accepts a string and adds it to the exception's
notes list. The standard traceback rendering includes all notes, in
the order they were added, after the exception.

   >>> try:
   ...     raise TypeError('bad type')
   ... except Exception as e:
   ...     e.add_note('Add some information')
   ...     e.add_note('Add some more information')
   ...     raise
   ...
   Traceback (most recent call last):
     File "<stdin>", line 2, in <module>
       raise TypeError('bad type')
   TypeError: bad type
   Add some information
   Add some more information
   >>>

For example, when collecting exceptions into an exception group, we
may want to add context information for the individual errors. In the
following each exception in the group has a note indicating when this
error has occurred.

   >>> def f():
   ...     raise OSError('operation failed')
   ...
   >>> excs = []
   >>> for i in range(3):
   ...     try:
   ...         f()
   ...     except Exception as e:
   ...         e.add_note(f'Happened in Iteration {i+1}')
   ...         excs.append(e)
   ...
   >>> raise ExceptionGroup('We have some problems', excs)
     + Exception Group Traceback (most recent call last):
     |   File "<stdin>", line 1, in <module>
     |     raise ExceptionGroup('We have some problems', excs)
     | ExceptionGroup: We have some problems (3 sub-exceptions)
     +-+---------------- 1 ----------------
       | Traceback (most recent call last):
       |   File "<stdin>", line 3, in <module>
       |     f()
       |     ~^^
       |   File "<stdin>", line 2, in f
       |     raise OSError('operation failed')
       | OSError: operation failed
       | Happened in Iteration 1
       +---------------- 2 ----------------
       | Traceback (most recent call last):
       |   File "<stdin>", line 3, in <module>
       |     f()
       |     ~^^
       |   File "<stdin>", line 2, in f
       |     raise OSError('operation failed')
       | OSError: operation failed
       | Happened in Iteration 2
       +---------------- 3 ----------------
       | Traceback (most recent call last):
       |   File "<stdin>", line 3, in <module>
       |     f()
       |     ~^^
       |   File "<stdin>", line 2, in f
       |     raise OSError('operation failed')
       | OSError: operation failed
       | Happened in Iteration 3
       +------------------------------------
   >>>
