# Exception Handling

## Single Exception

```py
import re

try:
    help()
except NameError as e:
    reveal_type(e)  # revealed: NameError
except re.error as f:
    reveal_type(f)  # revealed: error
```

## Unknown type in except handler does not cause spurious diagnostic

```py
from nonexistent_module import foo  # error: [unresolved-import]

try:
    help()
except foo as e:
    reveal_type(foo)  # revealed: Unknown
    reveal_type(e)  # revealed: Unknown
```

## Multiple Exceptions in a Tuple

```py
EXCEPTIONS = (AttributeError, TypeError)

try:
    help()
except (RuntimeError, OSError) as e:
    reveal_type(e)  # revealed: RuntimeError | OSError
except EXCEPTIONS as f:
    reveal_type(f)  # revealed: AttributeError | TypeError
```

## Dynamic exception types

```py
def foo(
    x: type[AttributeError],
    y: tuple[type[OSError], type[RuntimeError]],
    z: tuple[type[BaseException], ...],
    zz: tuple[type[TypeError | RuntimeError], ...],
    zzz: type[BaseException] | tuple[type[BaseException], ...],
    v: type[ValueError] | tuple[type[ValueError], ...],
):
    try:
        help()
    except x as e:
        reveal_type(e)  # revealed: AttributeError
    except y as f:
        reveal_type(f)  # revealed: OSError | RuntimeError
    except z as g:
        reveal_type(g)  # revealed: BaseException
    except zz as h:
        reveal_type(h)  # revealed: TypeError | RuntimeError
    except zzz as i:
        reveal_type(i)  # revealed: BaseException
    except v as j:
        reveal_type(j)  # revealed: ValueError
```

## Exception-class expressions

Exception-class expressions can produce intersections, gradual refinements, or unions. Positive
class-object constraints project into the caught instance type; unmappable positive constraints and
exact-class negatives do not exclude valid subclass instances.

```py
from typing import Any
from ty_extensions import Intersection

class FirstError(Exception): ...
class SecondError(Exception): ...

def catch_intersection(exc: Intersection[type[FirstError], type[SecondError]]) -> None:
    try:
        help()
    except exc as caught:
        reveal_type(caught)  # revealed: FirstError & SecondError

def catch_proper_subclass(exc: Any) -> None:
    if exc != ValueError and issubclass(exc, ValueError):
        reveal_type(exc)  # revealed: Any & type[ValueError] & ~<class 'ValueError'>
        try:
            help()
        except exc as caught:
            reveal_type(caught)  # revealed: Any & ValueError

try:
    help()
except ZeroDivisionError and ValueError as caught:
    reveal_type(caught)  # revealed: ZeroDivisionError | ValueError
```

We do not emit an `invalid-exception-caught` if a class is caught that has `Any` or `Unknown` in its
MRO, as the dynamic element in the MRO could materialize to some subclass of `BaseException`:

```py
from compat import BASE_EXCEPTION_CLASS  # error: [unresolved-import] "Cannot resolve imported module `compat`"

class Error(BASE_EXCEPTION_CLASS): ...

try:
    pass
except Error as err:
    pass
```

## Exception with no captured type

```py
try:
    {}.get("foo")
except TypeError:
    pass
```

## Exception which catches typevar

```toml
[environment]
python-version = "3.12"
```

```py
from typing import Callable, TypeVar

E = TypeVar("E", bound=Exception)

def silence[T: type[BaseException]](
    func: Callable[[], None],
    exception_type: T,
):
    try:
        func()
    except exception_type as e:
        reveal_type(e)  # revealed: T'instance@silence

def silence2[T: (type[ValueError], type[TypeError])](func: Callable[[], None], exception_type: T):
    try:
        func()
    except exception_type as e:
        reveal_type(e)  # revealed: T'instance@silence2

def silence3(func: Callable[[], None], exception_types: type[E] | tuple[type[E], ...]):
    try:
        func()
    except exception_types as e:
        reveal_type(e)  # revealed: E@silence3

def silence4[T: type[BaseException] | tuple[type[BaseException], ...]](
    func: Callable[[], None],
    exception_types: T,
):
    try:
        func()
    except exception_types as e:
        # TODO: Should reveal something more like `T'instance@silence4`.
        reveal_type(e)  # revealed: BaseException
```

## Invalid exception handlers

<!-- snapshot-diagnostics -->

```py
try:
    pass
# error: [invalid-exception-caught]
except 3 as e:
    reveal_type(e)  # revealed: Unknown

try:
    pass
# error: [invalid-exception-caught]
except (ValueError, OSError, "foo", b"bar") as e:
    reveal_type(e)  # revealed: ValueError | OSError | Unknown

def foo(
    x: type[str],
    y: tuple[type[OSError], type[RuntimeError], int],
    z: tuple[type[str], ...],
):
    try:
        help()
    # error: [invalid-exception-caught]
    except x as e:
        reveal_type(e)  # revealed: Unknown
    # error: [invalid-exception-caught]
    except y as f:
        reveal_type(f)  # revealed: OSError | RuntimeError | Unknown
    # error: [invalid-exception-caught]
    except z as g:
        reveal_type(g)  # revealed: Unknown

try:
    {}.get("foo")
# error: [invalid-exception-caught]
except int:
    pass
```

## Object raised is not an exception

```py
try:
    raise AttributeError()  # fine
except:
    pass

try:
    raise FloatingPointError  # fine
except:
    pass

try:
    raise 1  # error: [invalid-raise]
except:
    pass

try:
    raise int  # error: [invalid-raise]
except:
    pass

def _(e: Exception | type[Exception]):
    raise e  # fine

def _(e: Exception | type[Exception] | None):
    raise e  # error: [invalid-raise]
```

## Exception cause is not an exception

```py
def _():
    try:
        raise EOFError() from GeneratorExit  # fine
    except:
        pass

def _():
    try:
        raise StopIteration from MemoryError()  # fine
    except:
        pass

def _():
    try:
        raise BufferError() from None  # fine
    except:
        pass

def _():
    try:
        raise ZeroDivisionError from False  # error: [invalid-raise]
    except:
        pass

def _():
    try:
        raise SystemExit from bool()  # error: [invalid-raise]
    except:
        pass

def _():
    try:
        raise
    except KeyboardInterrupt as e:  # fine
        reveal_type(e)  # revealed: KeyboardInterrupt
        raise LookupError from e  # fine

def _():
    try:
        raise
    except int as e:  # error: [invalid-exception-caught]
        reveal_type(e)  # revealed: Unknown
        raise KeyError from e

def _(e: Exception | type[Exception]):
    raise ModuleNotFoundError from e  # fine

def _(e: Exception | type[Exception] | None):
    raise IndexError from e  # fine

def _(e: int | None):
    raise IndexError from e  # error: [invalid-raise]
```

## The caught exception is cleared at the end of the except clause

```py
e = None
reveal_type(e)  # revealed: None

try:
    raise ValueError()
except ValueError as e:
    reveal_type(e)  # revealed: ValueError
# error: [unresolved-reference]
reveal_type(e)  # revealed: Unknown

e = None

def cond() -> bool:
    return True

try:
    if cond():
        raise ValueError()
except ValueError as e:
    reveal_type(e)  # revealed: ValueError
# error: [possibly-unresolved-reference]
reveal_type(e)  # revealed: None

def f(x: type[Exception]):
    e = None
    try:
        raise x
    except ValueError as e:
        pass
    except:
        pass
    # error: [possibly-unresolved-reference]
    reveal_type(e)  # revealed: None
```

## Special-cased diagnostics for `NotImplemented`

<!-- snapshot-diagnostics -->

```py
try:
    # error: [invalid-raise]
    # error: [invalid-raise]
    raise NotImplemented from NotImplemented
# error: [invalid-exception-caught]
except NotImplemented:
    pass
# error: [invalid-exception-caught]
except (TypeError, NotImplemented):
    pass
```
