Repository: microsoft/debugpy

Issue:
Chained exception does not show cause message
I'm not really sure if this is a bug or a feature request or if this should be raised in `vscode-python` instead but it appears related to https://github.com/microsoft/debugpy/issues/391

## Environment data
- debugpy version: 1.6.3
- OS and version: Windows 10
- Python version (& distribution if applicable, e.g. Anaconda): Python 3.10
- Using VS Code or Visual Studio: VS Code

## Actual behavior

When a chained exception is caught it doesn't show the cause message, even though it shows the stack trace and says it was the cause.

![image](https://user-images.githubusercontent.com/5975405/185778156-f0f49edf-47d7-497b-b286-d16c30e98731.png)


## Expected  behavior
I think it should show:
1. The cause message
2. The cause stack trace
3.  > The above exception was the direct cause of the following exception:
4. The message
5. The stack trace

Alternatively 1 and 2 could be swapped as well as 4 and 5 swapped which would match what I see in the output:
```
Traceback (most recent call last):
  File "c:\Users\james.duley\dev\design-library-2\py-lib\src\holmes_design\junk.py", line 3, in <module>
    print(empty_dict["key"])
KeyError: 'key'

The above exception was the direct cause of the following exception:

Traceback (most recent call last):
  File "C:\Users\james.duley\AppData\Local\Programs\Python\Python310\lib\runpy.py", line 196, in _run_module_as_main    return _run_code(code, main_globals, None,
  File "C:\Users\james.duley\AppData\Local\Programs\Python\Python310\lib\runpy.py", line 86, in _run_code
    exec(code, run_globals)
  File "c:\Users\james.duley\.vscode\extensions\ms-python.python-2022.13.12301015\pythonFiles\lib\python\debugpy\__main__.py", line 39, in <module>
    cli.main()
  File "c:\Users\james.duley\.vscode\extensions\ms-python.python-2022.13.12301015\pythonFiles\lib\python\debugpy/..\debugpy\server\cli.py", line 430, in main
    run()
  File "c:\Users\james.duley\.vscode\extensions\ms-python.python-2022.13.12301015\pythonFiles\lib\python\debugpy/..\debugpy\server\cli.py", line 284, in run_file
    runpy.run_path(target, run_name="__main__")
  File "c:\Users\james.duley\.vscode\extensions\ms-python.python-2022.13.12301015\pythonFiles\lib\python\debugpy\_vendored\pydevd\_pydevd_bundle\pydevd_runpy.py", line 321, in run_path
    return _run_module_code(code, init_globals, run_name,
  File "c:\Users\james.duley\.vscode\extensions\ms-python.python-2022.13.12301015\pythonFiles\lib\python\debugpy\_vendored\pydevd\_pydevd_bundle\pydevd_runpy.py", line 135, in _run_module_code
    _run_code(code, mod_globals, init_globals,
  File "c:\Users\james.duley\.vscode\extensions\ms-python.python-2022.13.12301015\pythonFiles\lib\python\debugpy\_vendored\pydevd\_pydevd_bundle\pydevd_runpy.py", line 124, in _run_code
    exec(code, run_globals)
  File "c:\Users\james.duley\dev\design-library-2\py-lib\src\holmes_design\junk.py", line 5, in <module>
    raise Exception("Something went wrong") from e
Exception: Something went wrong
```

## Steps to reproduce:
1. Run this code in debug mode in VS Code with "Uncaught Exceptions" breakpoint enabled
```
empty_dict = {}
try:
    print(empty_dict["key"])
except KeyError as e:
    raise Exception("Something went wrong") from e
```

Discussion:
sonarcloud[bot]: Kudos, SonarCloud Quality Gate passed!&nbsp; &nbsp; [![Quality Gate passed](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/QualityGateBadge/passed-16px.png 'Quality Gate passed')](https://sonarcloud.io/dashboard?id=microsoft_debugpy&pullRequest=1057)

[![Bug](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/bug-16px.png 'Bug')](https://sonarcloud.io/project/issues?id=microsoft_debugpy&pullRequest=1057&resolved=false&types=BUG) [![A](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A-16px.png 'A')](https://sonarcloud.io/project/issues?id=microsoft_debugpy&pullRequest=1057&resolved=false&types=BUG) [0 Bugs](https://sonarcloud.io/project/issues?id=microsoft_debugpy&pullRequest=1057&resolved=false&types=BUG)  
[![Vulnerability](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/vulnerability-16px.png 'Vulnerability')](https://sonarcloud.io/project/issues?id=microsoft_debugpy&pullRequest=1057&resolved=false&types=VULNERABILITY) [![A](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A-16px.png 'A')](https://sonarcloud.io/project/issues?id=microsoft_debugpy&pullRequest=1057&resolved=false&types=VULNERABILITY) [0 Vulnerabilities](https://sonarcloud.io/project/issues?id=microsoft_debugpy&pullRequest=1057&resolved=false&types=VULNERABILITY)  
[![Security Hotspot](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/security_hotspot-16px.png 'Security Hotspot')](https://sonarcloud.io/project/security_hotspots?id=microsoft_debugpy&pullRequest=1057&resolved=false&types=SECURITY_HOTSPOT) [![A](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A-16px.png 'A')](https://sonarcloud.io/project/security_hotspots?id=microsoft_debugpy&pullRequest=1057&resolved=false&types=SECURITY_HOTSPOT) [0 Security Hotspots](https://sonarcloud.io/project/security_hotspots?id=microsoft_debugpy&pullRequest=1057&resolved=false&types=SECURITY_HOTSPOT)  
[![Code Smell](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/code_smell-16px.png 'Code Smell')](https://sonarcloud.io/project/issues?id=microsoft_debugpy&pullRequest=1057&resolved=false&types=CODE_SMELL) [![A](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A-16px.png 'A')](https://sonarcloud.io/project/issues?id=microsoft_debugpy&pullRequest=1057&resolved=false&types=CODE_SMELL) [0 Code Smells](https://sonarcloud.io/project/issues?id=microsoft_debugpy&pullRequest=1057&resolved=false&types=CODE_SMELL)

[![No Coverage information](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/CoverageChart/NoCoverageInfo-16px.png 'No Coverage information')](https://sonarcloud.io/component_measures?id=microsoft_debugpy&pullRequest=1057&metric=coverage&view=list) No Coverage information  
[![No Duplication information](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/Duplications/NoDuplicationInfo-16px.png 'No Duplication information')](https://sonarcloud.io/component_measures?id=microsoft_debugpy&pullRequest=1057&metric=duplicated_lines_density&view=list) No Duplication information


sonarcloud[bot]: Kudos, SonarCloud Quality Gate passed!&nbsp; &nbsp; [![Quality Gate passed](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/QualityGateBadge/passed-16px.png 'Quality Gate passed')](https://sonarcloud.io/dashboard?id=microsoft_debugpy&pullRequest=1057)

[![Bug](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/bug-16px.png 'Bug')](https://sonarcloud.io/project/issues?id=microsoft_debugpy&pullRequest=1057&resolved=false&types=BUG) [![A](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A-16px.png 'A')](https://sonarcloud.io/project/issues?id=microsoft_debugpy&pullRequest=1057&resolved=false&types=BUG) [0 Bugs](https://sonarcloud.io/project/issues?id=microsoft_debugpy&pullRequest=1057&resolved=false&types=BUG)  
[![Vulnerability](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/vulnerability-16px.png 'Vulnerability')](https://sonarcloud.io/project/issues?id=microsoft_debugpy&pullRequest=1057&resolved=false&types=VULNERABILITY) [![A](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A-16px.png 'A')](https://sonarcloud.io/project/issues?id=microsoft_debugpy&pullRequest=1057&resolved=false&types=VULNERABILITY) [0 Vulnerabilities](https://sonarcloud.io/project/issues?id=microsoft_debugpy&pullRequest=1057&resolved=false&types=VULNERABILITY)  
[![Security Hotspot](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/security_hotspot-16px.png 'Security Hotspot')](https://sonarcloud.io/project/security_hotspots?id=microsoft_debugpy&pullRequest=1057&resolved=false&types=SECURITY_HOTSPOT) [![A](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A-16px.png 'A')](https://sonarcloud.io/project/security_hotspots?id=microsoft_debugpy&pullRequest=1057&resolved=false&types=SECURITY_HOTSPOT) [0 Security Hotspots](https://sonarcloud.io/project/security_hotspots?id=microsoft_debugpy&pullRequest=1057&resolved=false&types=SECURITY_HOTSPOT)  
[![Code Smell](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/code_smell-16px.png 'Code Smell')](https://sonarcloud.io/project/issues?id=microsoft_debugpy&pullRequest=1057&resolved=false&types=CODE_SMELL) [![A](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A-16px.png 'A')](https://sonarcloud.io/project/issues?id=microsoft_debugpy&pullRequest=1057&resolved=false&types=CODE_SMELL) [0 Code Smells](https://sonarcloud.io/project/issues?id=microsoft_debugpy&pullRequest=1057&resolved=false&types=CODE_SMELL)

[![No Coverage information](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/CoverageChart/NoCoverageInfo-16px.png 'No Coverage information')](https://sonarcloud.io/component_measures?id=microsoft_debugpy&pullRequest=1057&metric=coverage&view=list) No Coverage information  
[![No Duplication information](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/Duplications/NoDuplicationInfo-16px.png 'No Duplication information')](https://sonarcloud.io/component_measures?id=microsoft_debugpy&pullRequest=1057&metric=duplicated_lines_density&view=list) No Duplication information


sonarcloud[bot]: Kudos, SonarCloud Quality Gate passed!&nbsp; &nbsp; [![Quality Gate passed](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/QualityGateBadge/passed-16px.png 'Quality Gate passed')](https://sonarcloud.io/dashboard?id=microsoft_debugpy&pullRequest=1057)

[![Bug](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/bug-16px.png 'Bug')](https://sonarcloud.io/project/issues?id=microsoft_debugpy&pullRequest=1057&resolved=false&types=BUG) [![A](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A-16px.png 'A')](https://sonarcloud.io/project/issues?id=microsoft_debugpy&pullRequest=1057&resolved=false&types=BUG) [0 Bugs](https://sonarcloud.io/project/issues?id=microsoft_debugpy&pullRequest=1057&resolved=false&types=BUG)  
[![Vulnerability](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/vulnerability-16px.png 'Vulnerability')](https://sonarcloud.io/project/issues?id=microsoft_debugpy&pullRequest=1057&resolved=false&types=VULNERABILITY) [![A](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A-16px.png 'A')](https://sonarcloud.io/project/issues?id=microsoft_debugpy&pullRequest=1057&resolved=false&types=VULNERABILITY) [0 Vulnerabilities](https://sonarcloud.io/project/issues?id=microsoft_debugpy&pullRequest=1057&resolved=false&types=VULNERABILITY)  
[![Security Hotspot](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/security_hotspot-16px.png 'Security Hotspot')](https://sonarcloud.io/project/security_hotspots?id=microsoft_debugpy&pullRequest=1057&resolved=false&types=SECURITY_HOTSPOT) [![A](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A-16px.png 'A')](https://sonarcloud.io/project/security_hotspots?id=microsoft_debugpy&pullRequest=1057&resolved=false&types=SECURITY_HOTSPOT) [0 Security Hotspots](https://sonarcloud.io/project/security_hotspots?id=microsoft_debugpy&pullRequest=1057&resolved=false&types=SECURITY_HOTSPOT)  
[![Code Smell](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/code_smell-16px.png 'Code Smell')](https://sonarcloud.io/project/issues?id=microsoft_debugpy&pullRequest=1057&resolved=false&types=CODE_SMELL) [![A](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A-16px.png 'A')](https://sonarcloud.io/project/issues?id=microsoft_debugpy&pullRequest=1057&resolved=false&types=CODE_SMELL) [0 Code Smells](https://sonarcloud.io/project/issues?id=microsoft_debugpy&pullRequest=1057&resolved=false&types=CODE_SMELL)

[![No Coverage information](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/CoverageChart/NoCoverageInfo-16px.png 'No Coverage information')](https://sonarcloud.io/component_measures?id=microsoft_debugpy&pullRequest=1057&metric=coverage&view=list) No Coverage information  
[![No Duplication information](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/Duplications/NoDuplicationInfo-16px.png 'No Duplication information')](https://sonarcloud.io/component_measures?id=microsoft_debugpy&pullRequest=1057&metric=duplicated_lines_density&view=list) No Duplication information



Patch:
diff --git a/src/debugpy/_vendored/pydevd/_pydevd_bundle/pydevd_comm.py b/src/debugpy/_vendored/pydevd/_pydevd_bundle/pydevd_comm.py
index 163fd7ab..8fd52229 100644
--- a/src/debugpy/_vendored/pydevd/_pydevd_bundle/pydevd_comm.py
+++ b/src/debugpy/_vendored/pydevd/_pydevd_bundle/pydevd_comm.py
@@ -1219,7 +1219,7 @@ def internal_evaluate_expression_json(py_db, request, thread_id):
         if context == 'repl' and eval_result is None:
             # We don't want "None" to appear when typing in the repl.
             body = pydevd_schema.EvaluateResponseBody(
-                result=None,
+                result='',
                 variablesReference=0,
             )
 
@@ -1420,7 +1420,6 @@ def build_exception_info_response(dbg, thread_id, request_seq, set_additional_th
         try:
             try:
                 frames_list = dbg.suspended_frames_manager.get_frames_list(thread_id)
-                memo = set()
                 while frames_list is not None and len(frames_list):
                     frames = []
 
@@ -1467,11 +1466,23 @@ def build_exception_info_response(dbg, thread_id, request_seq, set_additional_th
                         source_path = frames[0][0]
 
                     stack_str = ''.join(traceback.format_list(frames[-max_frames:]))
+
+                    try:
+                        stype = frames_list.exc_type.__qualname__
+                        smod = frames_list.exc_type.__module__
+                        if smod not in ("__main__", "builtins"):
+                            if not isinstance(smod, str):
+                                smod = "<unknown>"
+                            stype = smod + '.' + stype
+                    except Exception:
+                        stype = '<unable to get exception type>'
+                        pydev_log.exception('Error getting exception type.')
+
+                    stack_str += '%s: %s\n' % (stype, frames_list.exc_desc)
                     stack_str += frames_list.exc_context_msg
                     stack_str_lst.append(stack_str)
 
-                    frames_list = create_frames_list_from_exception_cause(
-                        frames_list.trace_obj, None, frames_list.exc_type, frames_list.exc_desc, memo)
+                    frames_list = frames_list.chained_frames_list
                     if frames_list is None or not frames_list:
                         break
 
diff --git a/src/debugpy/_vendored/pydevd/_pydevd_bundle/pydevd_frame_utils.py b/src/debugpy/_vendored/pydevd/_pydevd_bundle/pydevd_frame_utils.py
index c34ed304..c6786c0f 100644
--- a/src/debugpy/_vendored/pydevd/_pydevd_bundle/pydevd_frame_utils.py
+++ b/src/debugpy/_vendored/pydevd/_pydevd_bundle/pydevd_frame_utils.py
@@ -97,6 +97,8 @@ class FramesList(object):
         # This is to know whether an exception was extracted from a __cause__ or __context__.
         self.exc_context_msg = ''
 
+        self.chained_frames_list = None
+
     def append(self, frame):
         self._frames.append(frame)
 
@@ -128,7 +130,13 @@ class FramesList(object):
             lst.append('\n    ')
             lst.append(repr(frame))
             lst.append(',')
+
+        if self.chained_frames_list is not None:
+            lst.append('\n--- Chained ---\n')
+            lst.append(str(self.chained_frames_list))
+
         lst.append('\n)')
+
         return ''.join(lst)
 
     __str__ = __repr__
@@ -142,7 +150,8 @@ class _DummyFrameWrapper(object):
         self.f_back = f_back
         self.f_trace = None
         original_code = frame.f_code
-        self.f_code = FCode(original_code.co_name , original_code.co_filename)
+        name = original_code.co_name
+        self.f_code = FCode(name, original_code.co_filename)
 
     @property
     def f_locals(self):
@@ -152,6 +161,11 @@ class _DummyFrameWrapper(object):
     def f_globals(self):
         return self._base_frame.f_globals
 
+    def __str__(self):
+        return "<_DummyFrameWrapper, file '%s', line %s, %s" % (self.f_code.co_filename, self.f_lineno, self.f_code.co_name)
+
+    __repr__ = __str__
+
 
 _cause_message = (
     "\nThe above exception was the direct cause "
@@ -231,36 +245,6 @@ def create_frames_list_from_traceback(trace_obj, frame, exc_type, exc_desc, exce
         lst.append((tb.tb_frame, tb.tb_lineno))
         tb = tb.tb_next
 
-    curr = exc_desc
-    memo = set()
-    while True:
-        initial = curr
-        try:
-            curr = getattr(initial, '__cause__', None)
-        except Exception:
-            curr = None
-
-        if curr is None:
-            try:
-                curr = getattr(initial, '__context__', None)
-            except Exception:
-                curr = None
-
-        if curr is None or id(curr) in memo:
-            break
-
-        # The traceback module does this, so, let's play safe here too...
-        memo.add(id(curr))
-
-        tb = getattr(curr, '__traceback__', None)
-
-        while tb is not None:
-            # Note: we don't use the actual tb.tb_frame because if the cause of the exception
-            # uses the same frame object, the id(frame) would be the same and the frame_id_to_lineno
-            # would be wrong as the same frame needs to appear with 2 different lines.
-            lst.append((_DummyFrameWrapper(tb.tb_frame, tb.tb_lineno, None), tb.tb_lineno))
-            tb = tb.tb_next
-
     frames_list = None
 
     for tb_frame, tb_lineno in reversed(lst):
@@ -290,6 +274,18 @@ def create_frames_list_from_traceback(trace_obj, frame, exc_type, exc_desc, exce
         if len(frames_list) > 0:
             frames_list.current_frame = frames_list.last_frame()
 
+    curr = frames_list
+    memo = set()
+    memo.add(id(exc_desc))
+
+    while True:
+        chained = create_frames_list_from_exception_cause(None, None, None, curr.exc_desc, memo)
+        if chained is None:
+            break
+        else:
+            curr.chained_frames_list = chained
+            curr = chained
+
     return frames_list
 
 
diff --git a/src/debugpy/_vendored/pydevd/_pydevd_bundle/pydevd_net_command_factory_json.py b/src/debugpy/_vendored/pydevd/_pydevd_bundle/pydevd_net_command_factory_json.py
index 23746150..2d7a8046 100644
--- a/src/debugpy/_vendored/pydevd/_pydevd_bundle/pydevd_net_command_factory_json.py
+++ b/src/debugpy/_vendored/pydevd/_pydevd_bundle/pydevd_net_command_factory_json.py
@@ -230,7 +230,7 @@ class NetCommandFactoryJson(NetCommandFactory):
                     frames_list = pydevd_frame_utils.create_frames_list_from_frame(topmost_frame)
 
             for frame_id, frame, method_name, original_filename, filename_in_utf8, lineno, applied_mapping, show_as_current_frame in self._iter_visible_frames_info(
-                    py_db, frames_list
+                    py_db, frames_list, flatten_chained=True
                 ):
 
                 try:
diff --git a/src/debugpy/_vendored/pydevd/_pydevd_bundle/pydevd_net_command_factory_xml.py b/src/debugpy/_vendored/pydevd/_pydevd_bundle/pydevd_net_command_factory_xml.py
index 5ed644be..da2a680b 100644
--- a/src/debugpy/_vendored/pydevd/_pydevd_bundle/pydevd_net_command_factory_xml.py
+++ b/src/debugpy/_vendored/pydevd/_pydevd_bundle/pydevd_net_command_factory_xml.py
@@ -164,33 +164,46 @@ class NetCommandFactory(object):
         except:
             return self.make_error_message(0, get_exception_traceback_str())
 
-    def _iter_visible_frames_info(self, py_db, frames_list):
+    def _iter_visible_frames_info(self, py_db, frames_list, flatten_chained=False):
         assert frames_list.__class__ == FramesList
-        for frame in frames_list:
-            show_as_current_frame = frame is frames_list.current_frame
-            if frame.f_code is None:
-                pydev_log.info('Frame without f_code: %s', frame)
-                continue  # IronPython sometimes does not have it!
+        is_chained = False
+        while True:
+            for frame in frames_list:
+                show_as_current_frame = frame is frames_list.current_frame
+                if frame.f_code is None:
+                    pydev_log.info('Frame without f_code: %s', frame)
+                    continue  # IronPython sometimes does not have it!
 
-            method_name = frame.f_code.co_name  # method name (if in method) or ? if global
-            if method_name is None:
-                pydev_log.info('Frame without co_name: %s', frame)
-                continue  # IronPython sometimes does not have it!
+                method_name = frame.f_code.co_name  # method name (if in method) or ? if global
+                if method_name is None:
+                    pydev_log.info('Frame without co_name: %s', frame)
+                    continue  # IronPython sometimes does not have it!
 
-            abs_path_real_path_and_base = get_abs_path_real_path_and_base_from_frame(frame)
-            if py_db.get_file_type(frame, abs_path_real_path_and_base) == py_db.PYDEV_FILE:
-                # Skip pydevd files.
-                frame = frame.f_back
-                continue
+                if is_chained:
+                    method_name = '[Chained Exc: %s] %s' % (frames_list.exc_desc, method_name)
 
-            frame_id = id(frame)
-            lineno = frames_list.frame_id_to_lineno.get(frame_id, frame.f_lineno)
+                abs_path_real_path_and_base = get_abs_path_real_path_and_base_from_frame(frame)
+                if py_db.get_file_type(frame, abs_path_real_path_and_base) == py_db.PYDEV_FILE:
+                    # Skip pydevd files.
+                    frame = frame.f_back
+                    continue
 
-            filename_in_utf8, lineno, changed = py_db.source_mapping.map_to_client(abs_path_real_path_and_base[0], lineno)
-            new_filename_in_utf8, applied_mapping = pydevd_file_utils.map_file_to_client(filename_in_utf8)
-            applied_mapping = applied_mapping or changed
+                frame_id = id(frame)
+                lineno = frames_list.frame_id_to_lineno.get(frame_id, frame.f_lineno)
 
-            yield frame_id, frame, method_name, abs_path_real_path_and_base[0], new_filename_in_utf8, lineno, applied_mapping, show_as_current_frame
+                filename_in_utf8, lineno, changed = py_db.source_mapping.map_to_client(abs_path_real_path_and_base[0], lineno)
+                new_filename_in_utf8, applied_mapping = pydevd_file_utils.map_file_to_client(filename_in_utf8)
+                applied_mapping = applied_mapping or changed
+
+                yield frame_id, frame, method_name, abs_path_real_path_and_base[0], new_filename_in_utf8, lineno, applied_mapping, show_as_current_frame
+
+            if not flatten_chained:
+                break
+
+            frames_list = frames_list.chained_frames_list
+            if frames_list is None or len(frames_list) == 0:
+                break
+            is_chained = True
 
     def make_thread_stack_str(self, py_db, frames_list):
         assert frames_list.__class__ == FramesList
@@ -200,7 +213,7 @@ class NetCommandFactory(object):
 
         try:
             for frame_id, frame, method_name, _original_filename, filename_in_utf8, lineno, _applied_mapping, _show_as_current_frame in self._iter_visible_frames_info(
-                    py_db, frames_list
+                    py_db, frames_list, flatten_chained=True
                 ):
 
                 # print("file is ", filename_in_utf8)


Test patch:
diff --git a/src/debugpy/_vendored/pydevd/tests_python/resources/_debugger_case_raise_with_cause_msg.py b/src/debugpy/_vendored/pydevd/tests_python/resources/_debugger_case_raise_with_cause_msg.py
new file mode 100644
index 00000000..b9709f08
--- /dev/null
+++ b/src/debugpy/_vendored/pydevd/tests_python/resources/_debugger_case_raise_with_cause_msg.py
@@ -0,0 +1,12 @@
+def method2():
+    {}['foo']
+
+
+def method():
+    try:
+        method2()
+    except Exception as e:
+        raise Exception('TEST SUCEEDED') from e
+
+
+method()
diff --git a/src/debugpy/_vendored/pydevd/tests_python/test_debugger.py b/src/debugpy/_vendored/pydevd/tests_python/test_debugger.py
index 9262185d..7057252a 100644
--- a/src/debugpy/_vendored/pydevd/tests_python/test_debugger.py
+++ b/src/debugpy/_vendored/pydevd/tests_python/test_debugger.py
@@ -1657,13 +1657,13 @@ def test_case_throw_exc_reason_xml(case_setup):
             name_and_lines.append((frame['name'], frame['line']))
 
         assert name_and_lines == [
-            ('method2', '2'),
-            ('method', '6'),
-            ('foobar', '16'),
-            ('handle', '10'),
-            ('foobar', '18'),
             ('foobar', '20'),
             ('<module>', '23'),
+            ('[Chained Exc: another while handling] foobar', '18'),
+            ('[Chained Exc: another while handling] handle', '10'),
+            ('[Chained Exc: TEST SUCEEDED] foobar', '16'),
+            ('[Chained Exc: TEST SUCEEDED] method', '6'),
+            ('[Chained Exc: TEST SUCEEDED] method2', '2'),
         ]
 
         hit = writer.wait_for_breakpoint_hit(REASON_UNCAUGHT_EXCEPTION)
diff --git a/src/debugpy/_vendored/pydevd/tests_python/test_debugger_json.py b/src/debugpy/_vendored/pydevd/tests_python/test_debugger_json.py
index 810eafe8..a357cfd8 100644
--- a/src/debugpy/_vendored/pydevd/tests_python/test_debugger_json.py
+++ b/src/debugpy/_vendored/pydevd/tests_python/test_debugger_json.py
@@ -719,7 +719,6 @@ def test_case_handled_exception_no_break_on_generator(case_setup):
         writer.finished_ok = True
 
 
-@pytest.mark.skipif(not IS_PY36_OR_GREATER, reason='Requires Python 3.')
 def test_case_throw_exc_reason(case_setup):
 
     def check_test_suceeded_msg(self, stdout, stderr):
@@ -752,7 +751,15 @@ def test_case_throw_exc_reason(case_setup):
         stack_frames = json_hit.stack_trace_response.body.stackFrames
         # Note that the additional context doesn't really appear in the stack
         # frames, only in the details.
-        assert [x['name'] for x in stack_frames] == ['foobar', '<module>']
+        assert [x['name'] for x in stack_frames] == [
+            'foobar',
+            '<module>',
+            '[Chained Exc: another while handling] foobar',
+            '[Chained Exc: another while handling] handle',
+            '[Chained Exc: TEST SUCEEDED] foobar',
+            '[Chained Exc: TEST SUCEEDED] method',
+            '[Chained Exc: TEST SUCEEDED] method2',
+        ]
 
         body = exc_info_response.body
         assert body.exceptionId.endswith('RuntimeError')
@@ -761,11 +768,69 @@ def test_case_throw_exc_reason(case_setup):
 
         # Check that we have all the lines (including the cause/context) in the stack trace.
         import re
-        lines_and_names = re.findall(r',\sline\s(\d+),\sin\s([\w|<|>]+)', body.details.stackTrace)
+        lines_and_names = re.findall(r',\sline\s(\d+),\sin\s(\[Chained Exception\]\s)?([\w|<|>]+)', body.details.stackTrace)
         assert lines_and_names == [
-            ('16', 'foobar'), ('6', 'method'), ('2', 'method2'), ('18', 'foobar'), ('10', 'handle'), ('20', 'foobar'), ('23', '<module>')
+            ('16', '', 'foobar'),
+            ('6', '', 'method'),
+            ('2', '', 'method2'),
+            ('18', '', 'foobar'),
+            ('10', '', 'handle'),
+            ('20', '', 'foobar'),
+            ('23', '', '<module>'),
+        ], 'Did not find the expected names in:\n%s' % (body.details.stackTrace,)
+
+        json_facade.write_continue()
+
+        writer.finished_ok = True
+
+
+def test_case_throw_exc_reason_shown(case_setup):
+
+    def check_test_suceeded_msg(self, stdout, stderr):
+        return 'TEST SUCEEDED' in ''.join(stderr)
+
+    def additional_output_checks(writer, stdout, stderr):
+        assert "raise Exception('TEST SUCEEDED') from e" in stderr
+        assert "{}['foo']" in stderr
+        assert "KeyError: 'foo'" in stderr
+
+    with case_setup.test_file(
+            '_debugger_case_raise_with_cause_msg.py',
+            EXPECTED_RETURNCODE=1,
+            check_test_suceeded_msg=check_test_suceeded_msg,
+            additional_output_checks=additional_output_checks
+        ) as writer:
+        json_facade = JsonFacade(writer)
+
+        json_facade.write_launch(justMyCode=False)
+        json_facade.write_set_exception_breakpoints(['uncaught'])
+        json_facade.write_make_initial_run()
+
+        json_hit = json_facade.wait_for_thread_stopped(
+            reason='exception', line=writer.get_line_index_with_content("raise Exception('TEST SUCEEDED') from e"))
+
+        exc_info_request = json_facade.write_request(
+            pydevd_schema.ExceptionInfoRequest(pydevd_schema.ExceptionInfoArguments(json_hit.thread_id)))
+        exc_info_response = json_facade.wait_for_response(exc_info_request)
+
+        stack_frames = json_hit.stack_trace_response.body.stackFrames
+        # Note that the additional context doesn't really appear in the stack
+        # frames, only in the details.
+        assert [x['name'] for x in stack_frames] == [
+            'method',
+            '<module>',
+            "[Chained Exc: 'foo'] method",
+            "[Chained Exc: 'foo'] method2",
         ]
 
+        body = exc_info_response.body
+        assert body.exceptionId == 'Exception'
+        assert body.description == 'TEST SUCEEDED'
+        assert normcase(body.details.kwargs['source']) == normcase(writer.TEST_FILE)
+
+        # Check that we have the exception cause in the stack trace.
+        assert "KeyError: 'foo'" in body.details.stackTrace
+
         json_facade.write_continue()
 
         writer.finished_ok = True
@@ -1879,6 +1944,32 @@ def test_warning_on_repl(case_setup):
         writer.finished_ok = True
 
 
+def test_evaluate_none(case_setup, pyfile):
+
+    @pyfile
+    def eval_none():
+        print('TEST SUCEEDED')  # break here
+
+    with case_setup.test_file(eval_none) as writer:
+        json_facade = JsonFacade(writer)
+
+        json_facade.write_launch(justMyCode=False)
+
+        json_facade.write_set_breakpoints(writer.get_line_index_with_content('break here'))
+        json_facade.write_make_initial_run()
+
+        json_hit = json_facade.wait_for_thread_stopped()
+        json_hit = json_facade.get_stack_as_json_hit(json_hit.thread_id)
+
+        evaluate_response = json_facade.evaluate('None', json_hit.frame_id, context='repl')
+        assert evaluate_response.body.result is not None
+        assert evaluate_response.body.result == ''
+
+        json_facade.write_continue()
+
+        writer.finished_ok = True
+
+
 def test_evaluate_numpy(case_setup, pyfile):
     try:
         import numpy
@@ -3251,7 +3342,7 @@ def test_exception_details(case_setup, max_frames):
         else:
             json_facade.write_launch(maxExceptionStackFrames=max_frames)
             min_expected_lines = 10
-            max_expected_lines = 21
+            max_expected_lines = 22
 
         json_facade.write_set_exception_breakpoints(['raised'])
 
diff --git a/src/debugpy/_vendored/pydevd/tests_python/test_frame_utils.py b/src/debugpy/_vendored/pydevd/tests_python/test_frame_utils.py
new file mode 100644
index 00000000..2e595d4f
--- /dev/null
+++ b/src/debugpy/_vendored/pydevd/tests_python/test_frame_utils.py
@@ -0,0 +1,34 @@
+import sys
+from _pydevd_bundle.pydevd_constants import EXCEPTION_TYPE_USER_UNHANDLED
+
+
+def test_create_frames_list_from_traceback():
+
+    def method():
+        raise RuntimeError('first')
+
+    def method1():
+        try:
+            method()
+        except Exception as e:
+            raise RuntimeError('second') from e
+
+    def method2():
+        try:
+            method1()
+        except Exception as e:
+            raise RuntimeError('third') from e
+
+    try:
+        method2()
+    except Exception as e:
+        exc_type, exc_desc, trace_obj = sys.exc_info()
+        frame = sys._getframe()
+
+        from _pydevd_bundle.pydevd_frame_utils import create_frames_list_from_traceback
+        frames_list = create_frames_list_from_traceback(trace_obj, frame, exc_type, exc_desc, exception_type=EXCEPTION_TYPE_USER_UNHANDLED)
+        assert str(frames_list.exc_desc) == 'third'
+        assert str(frames_list.chained_frames_list.exc_desc) == 'second'
+        assert str(frames_list.chained_frames_list.chained_frames_list.exc_desc) == 'first'
+        assert frames_list.chained_frames_list.chained_frames_list.chained_frames_list is None
+
diff --git a/tests/debugpy/test_exception.py b/tests/debugpy/test_exception.py
index 385f32b6..31f315e7 100644
--- a/tests/debugpy/test_exception.py
+++ b/tests/debugpy/test_exception.py
@@ -354,7 +354,7 @@ def test_exception_stack(pyfile, target, run, max_frames):
         max_frames, (min_expected_lines, max_expected_lines) = {
             "all": (0, (100, 221)),
             "default": (None, (100, 221)),
-            10: (10, (10, 21)),
+            10: (10, (10, 22)),
         }[max_frames]
         if max_frames is not None:
             session.config["maxExceptionStackFrames"] = max_frames