Repository: scikit-hep/awkward

Issue:
Error formatting is broken (an error in the error handling)
### Version of Awkward Array

HEAD

### Description and code to reproduce

I have a real error and should be getting a properly formatted error message, but there's an error in the error-handling.

To reproduce it:

```python
import awkward as ak
f = ak.Array([[1, 2, 3], [], [4, 5]]).layout.form
ak.from_buffers(f, 0, {"": b"\x00\x00\x00\x00\x00\x00\x00\x00"}, buffer_key="{form_key}")
```

The error message is

```
Traceback (most recent call last):
  File "/home/jpivarski/irishep/awkward/src/awkward/operations/ak_from_buffers.py", line 89, in from_buffers
    return _impl(
  File "/home/jpivarski/irishep/awkward/src/awkward/operations/ak_from_buffers.py", line 146, in _impl
    out = reconstitute(form, length, container, getkey, backend, byteorder, simplify)
  File "/home/jpivarski/irishep/awkward/src/awkward/operations/ak_from_buffers.py", line 349, in reconstitute
    raw_array = container[getkey(form, "offsets")]
KeyError: 'None'

The above exception was the direct cause of the following exception:

Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/home/jpivarski/irishep/awkward/src/awkward/operations/ak_from_buffers.py", line 89, in from_buffers
    return _impl(
  File "/home/jpivarski/irishep/awkward/src/awkward/_errors.py", line 56, in __exit__
    self.handle_exception(exception_type, exception_value)
  File "/home/jpivarski/irishep/awkward/src/awkward/_errors.py", line 71, in handle_exception
    raise self.decorate_exception(cls, exception)
KeyError: "'None'\n\nThis error occurred while calling\n\n    ak.from_buffers(\n        form = ListOffsetForm-instance\n        length = 0\n        container = {'': b'\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00'}\n        buffer_key = '{form_key}'\n        backend = 'cpu'\n        byteorder = '<'\n        highlevel = True\n        behavior = None\n    )"
```

The `KeyError: 'None'` is the actual error, and it was supposed to be decorated like this:

```
KeyError: 'None'

This error occurred while calling

    ak.from_buffers(
        form = ListOffsetForm-instance
        length = 0
        container = {'': b'\x00\x00\x00\x00\x00\x00\x00\x00'}
        buffer_key = '{form_key}'
        backend = 'cpu'
        byteorder = '<'
        highlevel = True
        behavior = None
```

The line number didn't shift between my copy of Awkward and `main`:

https://github.com/scikit-hep/awkward/blob/be876a0ae3d78de29a33c635cdcc75315c4b1740/src/awkward/_errors.py#L71

I'm in the second branch because I'm not using Python 3.11 (with its exception decorators).

```python
>>> sys.version_info
sys.version_info(major=3, minor=9, micro=15, releaselevel='final', serial=0)
```


Discussion:
This is a Python detail unique to `KeyError` (see [BPO](https://bugs.python.org/issue2651)) that we can't really work around. In fact, it's one of the motivations for the __note__ work we added in 2.1.x!

We could rewrite `KeyError`s by generating custom subclasses, but this would be surprising to users, I think. I'm not sure that there's a good way to solve this besides _not_ decorating `KeyError`s on <3.11.
Yikes! Then as a first step, let's not decorate `KeyError` (i.e. check `type(err) is KeyError`, not `isinstance(err, KeyError)`) and over time introduce subclasses for `KeyError` so that we can have decorated errors.
No, better idea: catch `KeyError` and re-raise it (pre-3.11) as a subclass that isn't much different from the builtin `KeyError` (maybe even with the name `KeyError`, but in our own module) but has the `__str__` that we want, so that it displays properly. That would take care of the issue once and for all, without us having to introduce specialized `KeyErrors` throughout the codebase.
> No, better idea: catch `KeyError` and re-raise it (pre-3.11)

Yes, we can just re-raise `KeyError`s in places that we anticipate them, and plan to remove this workaround (and exception class) by 3.11.

In general, it's better not to implement custom exception types unless they're needed (in this case, we need them). Therefore, I think we should _not_ make these error types public, so that users don't try and catch them. We don't export any of our error types _yet_ (that was anticipated as a need for dask-awkward, but I don't think we're catching it yet).

Resolving patch:
diff --git a/src/awkward/_errors.py b/src/awkward/_errors.py
--- a/src/awkward/_errors.py
+++ b/src/awkward/_errors.py
@@ -1,6 +1,7 @@
 # BSD 3-Clause License; see https://github.com/scikit-hep/awkward-1.0/blob/main/LICENSE
 from __future__ import annotations
 
+import builtins
 import sys
 import threading
 import warnings
@@ -31,6 +32,11 @@ def __call__(self):
         return self.func(*self.args, **self.kwargs)
 
 
+class KeyError(builtins.KeyError):
+    def __str__(self):
+        return super(Exception, self).__str__()
+
+
 class ErrorContext:
     # Any other threads should get a completely independent _slate.
     _slate = threading.local()
@@ -87,6 +93,9 @@ def decorate_exception(self, cls: type[E], exception: E) -> E:
                     + "\n\nSee if this has been reported at https://github.com/scikit-hep/awkward/issues"
                 )
                 new_exception.__cause__ = exception
+            elif issubclass(cls, builtins.KeyError):
+                new_exception = KeyError(self.format_exception(exception))
+                new_exception.__cause__ = exception
             else:
                 new_exception = cls(self.format_exception(exception))
                 new_exception.__cause__ = exception


Test patch: