"""
Our exception hierarchy:

* HTTPError
  x RequestError
    + TransportError
      - TimeoutException
        · ConnectTimeout
        · ReadTimeout
        · WriteTimeout
        · PoolTimeout
      - NetworkError
        · ConnectError
        · ReadError
        · WriteError
        · CloseError
      - ProtocolError
        · LocalProtocolError
        · RemoteProtocolError
      - ProxyError
      - UnsupportedProtocol
    + DecodingError
    + TooManyRedirects
    + RequestBodyUnavailable
  x HTTPStatusError
* InvalidURL
* NotRedirectResponse
* CookieConflict
* StreamError
  x StreamConsumed
  x ResponseNotRead
  x RequestNotRead
  x ResponseClosed
"""
import contextlib
import typing

import httpcore

if typing.TYPE_CHECKING:
    from ._models import Request, Response  # pragma: nocover


class HTTPError(Exception):
    """
    Base class for `RequestError` and `HTTPStatusError`.

    Useful for `try...except` blocks when issuing a request,
    and then calling `.raise_for_status()`.

    For example:

    ```
    try:
        response = httpx.get("https://www.example.com")
        response.raise_for_status()
    except httpx.HTTPError as exc:
        print(f"HTTP Exception for {exc.request.url} - {exc.message}")
    ```
    """

    def __init__(self, message: str, *, request: "Request") -> None:
        super().__init__(message)
        self.request = request


class RequestError(HTTPError):
    """
    Base class for all exceptions that may occur when issuing a `.request()`.
    """

    def __init__(self, message: str, *, request: "Request") -> None:
        super().__init__(message, request=request)


class TransportError(RequestError):
    """
    Base class for all exceptions that occur at the level of the Transport API.

    All of these exceptions also have an equivelent mapping in `httpcore`.
    """


# Timeout exceptions...


class TimeoutException(TransportError):
    """
    The base class for timeout errors.

    An operation has timed out.
    """


class ConnectTimeout(TimeoutException):
    """
    Timed out while connecting to the host.
    """


class ReadTimeout(TimeoutException):
    """
    Timed out while receiving data from the host.
    """


class WriteTimeout(TimeoutException):
    """
    Timed out while sending data to the host.
    """


class PoolTimeout(TimeoutException):
    """
    Timed out waiting to acquire a connection from the pool.
    """


# Core networking exceptions...


class NetworkError(TransportError):
    """
    The base class for network-related errors.

    An error occurred while interacting with the network.
    """


class ReadError(NetworkError):
    """
    Failed to receive data from the network.
    """


class WriteError(NetworkError):
    """
    Failed to send data through the network.
    """


class ConnectError(NetworkError):
    """
    Failed to establish a connection.
    """


class CloseError(NetworkError):
    """
    Failed to close a connection.
    """


# Other transport exceptions...


class ProxyError(TransportError):
    """
    An error occurred while establishing a proxy connection.
    """


class UnsupportedProtocol(TransportError):
    """
    Attempted to make a request to an unsupported protocol.

    For example issuing a request to `ftp://www.example.com`.
    """


class ProtocolError(TransportError):
    """
    The protocol was violated.
    """


class LocalProtocolError(ProtocolError):
    """
    A protocol was violated by the client.

    For example if the user instantiated a `Request` instance explicitly,
    failed to include the mandatory `Host:` header, and then issued it directly
    using `client.send()`.
    """


class RemoteProtocolError(ProtocolError):
    """
    The protocol was violated by the server.

    For exaample, returning malformed HTTP.
    """


# Other request exceptions...


class DecodingError(RequestError):
    """
    Decoding of the response failed, due to a malformed encoding.
    """


class TooManyRedirects(RequestError):
    """
    Too many redirects.
    """


class RequestBodyUnavailable(RequestError):
    """
    Had to send the request again, but the request body was streaming, and is
    no longer available.
    """


# Client errors


class HTTPStatusError(HTTPError):
    """
    The response had an error HTTP status of 4xx or 5xx.

    May be raised when calling `response.raise_for_status()`
    """

    def __init__(
        self, message: str, *, request: "Request", response: "Response"
    ) -> None:
        super().__init__(message, request=request)
        self.response = response


class InvalidURL(Exception):
    """
    URL is improperly formed or cannot be parsed.
    """

    def __init__(self, message: str) -> None:
        super().__init__(message)


class NotRedirectResponse(Exception):
    """
    Response was not a redirect response.

    May be raised if `response.next()` is called without first
    properly checking `response.is_redirect`.
    """

    def __init__(self, message: str) -> None:
        super().__init__(message)


class CookieConflict(Exception):
    """
    Attempted to lookup a cookie by name, but multiple cookies existed.

    Can occur when calling `response.cookies.get(...)`.
    """

    def __init__(self, message: str) -> None:
        super().__init__(message)


# Stream exceptions...

# These may occur as the result of a programming error, by accessing
# the request/response stream in an invalid manner.


class StreamError(Exception):
    """
    The base class for stream exceptions.

    The developer made an error in accessing the request stream in
    an invalid way.
    """

    def __init__(self, message: str) -> None:
        super().__init__(message)


class StreamConsumed(StreamError):
    """
    Attempted to read or stream response content, but the content has already
    been streamed.
    """

    def __init__(self) -> None:
        message = (
            "Attempted to read or stream response content, but the content has "
            "already been streamed."
        )
        super().__init__(message)


class ResponseNotRead(StreamError):
    """
    Attempted to access response content, without having called `read()`
    after a streaming response.
    """

    def __init__(self) -> None:
        message = (
            "Attempted to access response content, without having called `read()` "
            "after a streaming response."
        )
        super().__init__(message)


class RequestNotRead(StreamError):
    """
    Attempted to access request content, without having called `read()`.
    """

    def __init__(self) -> None:
        message = "Attempted to access request content, without having called `read()`."
        super().__init__(message)


class ResponseClosed(StreamError):
    """
    Attempted to read or stream response content, but the request has been
    closed.
    """

    def __init__(self) -> None:
        message = (
            "Attempted to read or stream response content, but the request has "
            "been closed."
        )
        super().__init__(message)


@contextlib.contextmanager
def map_exceptions(
    mapping: typing.Mapping[typing.Type[Exception], typing.Type[Exception]],
    **kwargs: typing.Any,
) -> typing.Iterator[None]:
    try:
        yield
    except Exception as exc:
        mapped_exc = None

        for from_exc, to_exc in mapping.items():
            if not isinstance(exc, from_exc):
                continue
            # We want to map to the most specific exception we can find.
            # Eg if `exc` is an `httpcore.ReadTimeout`, we want to map to
            # `httpx.ReadTimeout`, not just `httpx.TimeoutException`.
            if mapped_exc is None or issubclass(to_exc, mapped_exc):
                mapped_exc = to_exc

        if mapped_exc is None:
            raise

        message = str(exc)
        raise mapped_exc(message, **kwargs) from None  # type: ignore


HTTPCORE_EXC_MAP = {
    httpcore.TimeoutException: TimeoutException,
    httpcore.ConnectTimeout: ConnectTimeout,
    httpcore.ReadTimeout: ReadTimeout,
    httpcore.WriteTimeout: WriteTimeout,
    httpcore.PoolTimeout: PoolTimeout,
    httpcore.NetworkError: NetworkError,
    httpcore.ConnectError: ConnectError,
    httpcore.ReadError: ReadError,
    httpcore.WriteError: WriteError,
    httpcore.CloseError: CloseError,
    httpcore.ProxyError: ProxyError,
    httpcore.UnsupportedProtocol: UnsupportedProtocol,
    httpcore.ProtocolError: ProtocolError,
    httpcore.LocalProtocolError: LocalProtocolError,
    httpcore.RemoteProtocolError: RemoteProtocolError,
}

httpx/_exceptions.py
"""
Our exception hierarchy:

* HTTPError
  x RequestError
    + TransportError
      - TimeoutException
        · ConnectTimeout
        · ReadTimeout
        · WriteTimeout
        · PoolTimeout
      - NetworkError
        · ConnectError
        · ReadError
        · WriteError
        · CloseError
      - ProtocolError
        · LocalProtocolError
        · RemoteProtocolError
      - ProxyError
      - UnsupportedProtocol
    + DecodingError
    + TooManyRedirects
    + RequestBodyUnavailable
  x HTTPStatusError
* InvalidURL
* NotRedirectResponse
* CookieConflict
* StreamError
  x StreamConsumed
  x ResponseNotRead
  x RequestNotRead
  x ResponseClosed
"""
import contextlib
import typing

import httpcore

if typing.TYPE_CHECKING:
    from ._models import Request, Response  # pragma: nocover


class HTTPError(Exception):
    """
    Base class for `RequestError` and `HTTPStatusError`.

    Useful for `try...except` blocks when issuing a request,
    and then calling `.raise_for_status()`.

    For example:

    ```
    try:
        response = httpx.get("https://www.example.com")
        response.raise_for_status()
    except httpx.HTTPError as exc:
        print(f"HTTP Exception for {exc.request.url} - {exc.message}")
    ```
    """

    def __init__(self, message: str, *, request: "Request") -> None:
        super().__init__(message)
        self.request = request


class RequestError(HTTPError):
    """
    Base class for all exceptions that may occur when issuing a `.request()`.
    """

    def __init__(self, message: str, *, request: "Request") -> None:
        super().__init__(message, request=request)


class TransportError(RequestError):
    """
    Base class for all exceptions that occur at the level of the Transport API.

    All of these exceptions also have an equivelent mapping in `httpcore`.
    """


# Timeout exceptions...


class TimeoutException(TransportError):
    """
    The base class for timeout errors.

    An operation has timed out.
    """


class ConnectTimeout(TimeoutException):
    """
    Timed out while connecting to the host.
    """


class ReadTimeout(TimeoutException):
    """
    Timed out while receiving data from the host.
    """


class WriteTimeout(TimeoutException):
    """
    Timed out while sending data to the host.
    """


class PoolTimeout(TimeoutException):
    """
    Timed out waiting to acquire a connection from the pool.
    """


# Core networking exceptions...


class NetworkError(TransportError):
    """
    The base class for network-related errors.

    An error occurred while interacting with the network.
    """


class ReadError(NetworkError):
    """
    Failed to receive data from the network.
    """


class WriteError(NetworkError):
    """
    Failed to send data through the network.
    """


class ConnectError(NetworkError):
    """
    Failed to establish a connection.
    """


class CloseError(NetworkError):
    """
    Failed to close a connection.
    """


# Other transport exceptions...


class ProxyError(TransportError):
    """
    An error occurred while establishing a proxy connection.
    """


class UnsupportedProtocol(TransportError):
    """
    Attempted to make a request to an unsupported protocol.

    For example issuing a request to `ftp://www.example.com`.
    """


class ProtocolError(TransportError):
    """
    The protocol was violated.
    """


class LocalProtocolError(ProtocolError):
    """
    A protocol was violated by the client.

    For example if the user instantiated a `Request` instance explicitly,
    failed to include the mandatory `Host:` header, and then issued it directly
    using `client.send()`.
    """


class RemoteProtocolError(ProtocolError):
    """
    The protocol was violated by the server.

    For exaample, returning malformed HTTP.
    """


# Other request exceptions...


class DecodingError(RequestError):
    """
    Decoding of the response failed, due to a malformed encoding.
    """


class TooManyRedirects(RequestError):
    """
    Too many redirects.
    """


class RequestBodyUnavailable(RequestError):
    """
    Had to send the request again, but the request body was streaming, and is
    no longer available.
    """


# Client errors


class HTTPStatusError(HTTPError):
    """
    The response had an error HTTP status of 4xx or 5xx.

    May be raised when calling `response.raise_for_status()`
    """

    def __init__(
        self, message: str, *, request: "Request", response: "Response"
    ) -> None:
        super().__init__(message, request=request)
        self.response = response


class InvalidURL(Exception):
    """
    URL is improperly formed or cannot be parsed.
    """

    def __init__(self, message: str) -> None:
        super().__init__(message)


class NotRedirectResponse(Exception):
    """
    Response was not a redirect response.

    May be raised if `response.next()` is called without first
    properly checking `response.is_redirect`.
    """

    def __init__(self, message: str) -> None:
        super().__init__(message)


class CookieConflict(Exception):
    """
    Attempted to lookup a cookie by name, but multiple cookies existed.

    Can occur when calling `response.cookies.get(...)`.
    """

    def __init__(self, message: str) -> None:
        super().__init__(message)


# Stream exceptions...

# These may occur as the result of a programming error, by accessing
# the request/response stream in an invalid manner.


class StreamError(Exception):
    """
    The base class for stream exceptions.

    The developer made an error in accessing the request stream in
    an invalid way.
    """

    def __init__(self, message: str) -> None:
        super().__init__(message)


class StreamConsumed(StreamError):
    """
    Attempted to read or stream response content, but the content has already
    been streamed.
    """

    def __init__(self) -> None:
        message = (
            "Attempted to read or stream response content, but the content has "
            "already been streamed."
        )
        super().__init__(message)


class ResponseNotRead(StreamError):
    """
    Attempted to access response content, without having called `read()`
    after a streaming response.
    """

    def __init__(self) -> None:
        message = (
            "Attempted to access response content, without having called `read()` "
            "after a streaming response."
        )
        super().__init__(message)


class RequestNotRead(StreamError):
    """
    Attempted to access request content, without having called `read()`.
    """

    def __init__(self) -> None:
        message = "Attempted to access request content, without having called `read()`."
        super().__init__(message)


class ResponseClosed(StreamError):
    """
    Attempted to read or stream response content, but the request has been
    closed.
    """

    def __init__(self) -> None:
        message = (
            "Attempted to read or stream response content, but the request has "
            "been closed."
        )
        super().__init__(message)


@contextlib.contextmanager
def map_exceptions(
    mapping: typing.Mapping[typing.Type[Exception], typing.Type[Exception]],
    **kwargs: typing.Any,
) -> typing.Iterator[None]:
    try:
        yield
    except Exception as exc:
        mapped_exc = None

        for from_exc, to_exc in mapping.items():
            if not isinstance(exc, from_exc):
                continue
            # We want to map to the most specific exception we can find.
            # Eg if `exc` is an `httpcore.ReadTimeout`, we want to map to
            # `httpx.ReadTimeout`, not just `httpx.TimeoutException`.
            if mapped_exc is None or issubclass(to_exc, mapped_exc):
                mapped_exc = to_exc

        if mapped_exc is None:
            raise

        message = str(exc)
        raise mapped_exc(message, **kwargs) from exc  # type: ignore


HTTPCORE_EXC_MAP = {
    httpcore.TimeoutException: TimeoutException,
    httpcore.ConnectTimeout: ConnectTimeout,
    httpcore.ReadTimeout: ReadTimeout,
    httpcore.WriteTimeout: WriteTimeout,
    httpcore.PoolTimeout: PoolTimeout,
    httpcore.NetworkError: NetworkError,
    httpcore.ConnectError: ConnectError,
    httpcore.ReadError: ReadError,
    httpcore.WriteError: WriteError,
    httpcore.CloseError: CloseError,
    httpcore.ProxyError: ProxyError,
    httpcore.UnsupportedProtocol: UnsupportedProtocol,
    httpcore.ProtocolError: ProtocolError,
    httpcore.LocalProtocolError: LocalProtocolError,
    httpcore.RemoteProtocolError: RemoteProtocolError,
}

httpx/_exceptions.py
diff --git a/httpx/_exceptions.py b/httpx/_exceptions.py
index 9a46d7d24a..4d6837778a 100644
--- a/httpx/_exceptions.py
+++ b/httpx/_exceptions.py
@@ -356,7 +356,7 @@ def map_exceptions(
             raise
 
         message = str(exc)
-        raise mapped_exc(message, **kwargs) from None  # type: ignore
+        raise mapped_exc(message, **kwargs) from exc  # type: ignore
 
 
 HTTPCORE_EXC_MAP = {

How about attaching original traceback to exceptions raised in `map_exceptions()` ?
Currently we use `map_exceptions()` to covert underlying library exceptions to `httpx` exceptions, when captured an expected exception, we raise a new exception from `None`:  
https://github.com/encode/httpx/blob/655773e1c1b75895eda927d5a9d22a3b5b8f572d/httpx/_exceptions.py#L336-L359

This causes a traceback like this:
```
In [30]: httpx.get('http://notexisthost')
Traceback (most recent call last):
  File "<ipython-input-30-7c88a36ce394>", line 1, in <module>
    httpx.get('http://notexisthost')
  File "D:\programs\anaconda3\lib\site-packages\httpx\_api.py", line 170, in get
    trust_env=trust_env,
  File "D:\programs\anaconda3\lib\site-packages\httpx\_api.py", line 96, in request
    allow_redirects=allow_redirects,
  File "D:\programs\anaconda3\lib\site-packages\httpx\_client.py", line 601, in request
    request, auth=auth, allow_redirects=allow_redirects, timeout=timeout,
  File "D:\programs\anaconda3\lib\site-packages\httpx\_client.py", line 621, in send
    request, auth=auth, timeout=timeout, allow_redirects=allow_redirects,
  File "D:\programs\anaconda3\lib\site-packages\httpx\_client.py", line 648, in send_handling_redirects
    request, auth=auth, timeout=timeout, history=history
  File "D:\programs\anaconda3\lib\site-packages\httpx\_client.py", line 684, in send_handling_auth
    response = self.send_single_request(request, timeout)
  File "D:\programs\anaconda3\lib\site-packages\httpx\_client.py", line 719, in send_single_request
    timeout=timeout.as_dict(),
  File "D:\programs\anaconda3\lib\site-packages\httpcore\_sync\http_proxy.py", line 99, in request
    method, url, headers=headers, stream=stream, timeout=timeout
  File "D:\programs\anaconda3\lib\site-packages\httpcore\_sync\http_proxy.py", line 150, in _forward_request
    method, url, headers=headers, stream=stream, timeout=timeout
  File "D:\programs\anaconda3\lib\site-packages\httpcore\_sync\connection.py", line 78, in request
    return self.connection.request(method, url, headers, stream, timeout)
  File "D:\programs\anaconda3\lib\site-packages\httpcore\_sync\http11.py", line 62, in request
    ) = self._receive_response(timeout)
  File "D:\programs\anaconda3\lib\site-packages\httpcore\_sync\http11.py", line 115, in _receive_response
    event = self._receive_event(timeout)
  File "D:\programs\anaconda3\lib\site-packages\httpcore\_sync\http11.py", line 142, in _receive_event
    event = self.h11_state.next_event()
  File "D:\programs\anaconda3\lib\contextlib.py", line 130, in __exit__
    self.gen.throw(type, value, traceback)
  File "D:\programs\anaconda3\lib\site-packages\httpcore\_exceptions.py", line 12, in map_exceptions
    raise to_exc(exc) from None
ProtocolError: can't handle event type ConnectionClosed when role=SERVER and state=SEND_RESPONSE
```

Notice the last two frames from the traceback:
```
File "D:\programs\anaconda3\lib\contextlib.py", line 130, in __exit__
    self.gen.throw(type, value, traceback)
  File "D:\programs\anaconda3\lib\site-packages\httpcore\_exceptions.py", line 12, in map_exceptions
    raise to_exc(exc) from None
```
I find that It's not obvious to figure out where's the root cause. If we attach the original traceback to the newly raised exception like this:
```py
@contextlib.contextmanager
def map_exceptions(
    mapping: typing.Mapping[typing.Type[Exception], typing.Type[Exception]],
    **kwargs: typing.Any,
) -> typing.Iterator[None]:
    try:
        yield
    except Exception as exc:
        mapped_exc = None

        for from_exc, to_exc in mapping.items():
            if not isinstance(exc, from_exc):
                continue
            # We want to map to the most specific exception we can find.
            # Eg if `exc` is an `httpcore.ReadTimeout`, we want to map to
            # `httpx.ReadTimeout`, not just `httpx.TimeoutException`.
            if mapped_exc is None or issubclass(to_exc, mapped_exc):
                mapped_exc = to_exc

        if mapped_exc is None:
            raise

        message = str(exc)
        raise mapped_exc(message, **kwargs).with_traceback(exc.__traceback__) from None  # type: ignore
```
the traceback becomes:
```
In [4]: httpx.get('http://doestnotexist')
Traceback (most recent call last):
  File "<ipython-input-4-74881dccb3f2>", line 1, in <module>
    httpx.get('http://doestnotexist')
  File "e:\projects\pycharm\httpx\httpx\_api.py", line 170, in get
    trust_env=trust_env,
  File "e:\projects\pycharm\httpx\httpx\_api.py", line 96, in request
    allow_redirects=allow_redirects,
  File "e:\projects\pycharm\httpx\httpx\_client.py", line 643, in request
    request, auth=auth, allow_redirects=allow_redirects, timeout=timeout,
  File "e:\projects\pycharm\httpx\httpx\_client.py", line 673, in send
    request, auth=auth, timeout=timeout, allow_redirects=allow_redirects,
  File "e:\projects\pycharm\httpx\httpx\_client.py", line 702, in _send_handling_redirects
    request, auth=auth, timeout=timeout, history=history
  File "e:\projects\pycharm\httpx\httpx\_client.py", line 738, in _send_handling_auth
    response = self._send_single_request(request, timeout)
  File "e:\projects\pycharm\httpx\httpx\_client.py", line 772, in _send_single_request
    timeout=timeout.as_dict(),
  File "D:\programs\anaconda3\lib\contextlib.py", line 130, in __exit__
    self.gen.throw(type, value, traceback)
  File "e:\projects\pycharm\httpx\httpx\_exceptions.py", line 359, in map_exceptions
    raise mapped_exc(message, **kwargs).with_traceback(exc.__traceback__) from None  # type: ignore
  File "e:\projects\pycharm\httpx\httpx\_exceptions.py", line 342, in map_exceptions
    yield
  File "e:\projects\pycharm\httpx\httpx\_client.py", line 772, in _send_single_request
    timeout=timeout.as_dict(),
  File "E:\projects\pycharm\httpcore\httpcore\_sync\http_proxy.py", line 102, in request
    method, url, headers=headers, stream=stream, timeout=timeout
  File "E:\projects\pycharm\httpcore\httpcore\_sync\http_proxy.py", line 159, in _forward_request
    method, url, headers=headers, stream=stream, timeout=timeout
  File "E:\projects\pycharm\httpcore\httpcore\_sync\connection.py", line 96, in request
    return self.connection.request(method, url, headers, stream, timeout)
  File "E:\projects\pycharm\httpcore\httpcore\_sync\http11.py", line 73, in request
    ) = self._receive_response(timeout)
  File "E:\projects\pycharm\httpcore\httpcore\_sync\http11.py", line 130, in _receive_response
    event = self._receive_event(timeout)
  File "E:\projects\pycharm\httpcore\httpcore\_sync\http11.py", line 160, in _receive_event
    data = self.socket.read(self.READ_NUM_BYTES, timeout)
  File "E:\projects\pycharm\httpcore\httpcore\_backends\sync.py", line 64, in read
    raise ReadError("Server disconnected while attempting read")
ReadError: Server disconnected while attempting read
```
May be it will be easier to realize the really problems?
