8198801: JShell: user exception chained cause not retained
authorrfield
Tue, 03 Apr 2018 13:27:58 -0700
changeset 49515 083318155ad1
parent 49514 25695fce1601
child 49516 a6aca02cf01a
8198801: JShell: user exception chained cause not retained Reviewed-by: jlahoda
src/jdk.jshell/share/classes/jdk/internal/jshell/tool/JShellTool.java
src/jdk.jshell/share/classes/jdk/internal/jshell/tool/resources/l10n.properties
src/jdk.jshell/share/classes/jdk/jshell/Eval.java
src/jdk.jshell/share/classes/jdk/jshell/EvalException.java
src/jdk.jshell/share/classes/jdk/jshell/JShellException.java
src/jdk.jshell/share/classes/jdk/jshell/UnresolvedReferenceException.java
src/jdk.jshell/share/classes/jdk/jshell/execution/DirectExecutionControl.java
src/jdk.jshell/share/classes/jdk/jshell/execution/ExecutionControlForwarder.java
src/jdk.jshell/share/classes/jdk/jshell/execution/RemoteCodes.java
src/jdk.jshell/share/classes/jdk/jshell/execution/StreamingExecutionControl.java
test/langtools/jdk/jshell/ExceptionsTest.java
test/langtools/jdk/jshell/ToolSimpleTest.java
--- a/src/jdk.jshell/share/classes/jdk/internal/jshell/tool/JShellTool.java	Tue Apr 03 21:50:35 2018 +0200
+++ b/src/jdk.jshell/share/classes/jdk/internal/jshell/tool/JShellTool.java	Tue Apr 03 13:27:58 2018 -0700
@@ -81,6 +81,7 @@
 import jdk.jshell.ImportSnippet;
 import jdk.jshell.JShell;
 import jdk.jshell.JShell.Subscription;
+import jdk.jshell.JShellException;
 import jdk.jshell.MethodSnippet;
 import jdk.jshell.Snippet;
 import jdk.jshell.Snippet.Kind;
@@ -3357,21 +3358,61 @@
     /**
      * Print out a snippet exception.
      *
-     * @param exception the exception to print
+     * @param exception the throwable to print
      * @return true on fatal exception
      */
-    private boolean displayException(Exception exception) {
+    private boolean displayException(Throwable exception) {
+        Throwable rootCause = exception;
+        while (rootCause instanceof EvalException) {
+            rootCause = rootCause.getCause();
+        }
+        if (rootCause != exception && rootCause instanceof UnresolvedReferenceException) {
+            // An unresolved reference caused a chained exception, just show the unresolved
+            return displayException(rootCause, null);
+        } else {
+            return displayException(exception, null);
+        }
+    }
+    //where
+    private boolean displayException(Throwable exception, StackTraceElement[] caused) {
         if (exception instanceof EvalException) {
-            printEvalException((EvalException) exception);
-            return true;
+            // User exception
+            return displayEvalException((EvalException) exception, caused);
         } else if (exception instanceof UnresolvedReferenceException) {
-            printUnresolvedException((UnresolvedReferenceException) exception);
-            return false;
+            // Reference to an undefined snippet
+            return displayUnresolvedException((UnresolvedReferenceException) exception);
         } else {
+            // Should never occur
             error("Unexpected execution exception: %s", exception);
             return true;
         }
     }
+    //where
+    private boolean displayUnresolvedException(UnresolvedReferenceException ex) {
+        // Display the resolution issue
+        printSnippetStatus(ex.getSnippet(), false);
+        return false;
+    }
+
+    //where
+    private boolean displayEvalException(EvalException ex, StackTraceElement[] caused) {
+        // The message for the user exception is configured based on the
+        // existance of an exception message and if this is a recursive
+        // invocation for a chained exception.
+        String msg = ex.getMessage();
+        String key = "jshell.err.exception" +
+                (caused == null? ".thrown" : ".cause") +
+                (msg == null? "" : ".message");
+        errormsg(key, ex.getExceptionClassName(), msg);
+        // The caused trace is sent to truncate duplicate elements in the cause trace
+        printStackTrace(ex.getStackTrace(), caused);
+        JShellException cause = ex.getCause();
+        if (cause != null) {
+            // Display the cause (recursively)
+            displayException(cause, ex.getStackTrace());
+        }
+        return true;
+    }
 
     /**
      * Display a list of diagnostics.
@@ -3518,9 +3559,19 @@
         }
         return false;
     }
-    //where
-    void printStackTrace(StackTraceElement[] stes) {
-        for (StackTraceElement ste : stes) {
+
+    // Print a stack trace, elide frames displayed for the caused exception
+    void printStackTrace(StackTraceElement[] stes, StackTraceElement[] caused) {
+        int overlap = 0;
+        if (caused != null) {
+            int maxOverlap = Math.min(stes.length, caused.length);
+            while (overlap < maxOverlap
+                    && stes[stes.length - (overlap + 1)].equals(caused[caused.length - (overlap + 1)])) {
+                ++overlap;
+            }
+        }
+        for (int i = 0; i < stes.length - overlap; ++i) {
+            StackTraceElement ste = stes[i];
             StringBuilder sb = new StringBuilder();
             String cn = ste.getClassName();
             if (!cn.isEmpty()) {
@@ -3548,19 +3599,9 @@
             error("      at %s(%s)", sb, loc);
 
         }
-    }
-    //where
-    void printUnresolvedException(UnresolvedReferenceException ex) {
-        printSnippetStatus(ex.getSnippet(), false);
-    }
-    //where
-    void printEvalException(EvalException ex) {
-        if (ex.getMessage() == null) {
-            error("%s thrown", ex.getExceptionClassName());
-        } else {
-            error("%s thrown: %s", ex.getExceptionClassName(), ex.getMessage());
+        if (overlap != 0) {
+            error("      ...");
         }
-        printStackTrace(ex.getStackTrace());
     }
 
     private FormatAction toAction(Status status, Status previousStatus, boolean isSignatureChange) {
--- a/src/jdk.jshell/share/classes/jdk/internal/jshell/tool/resources/l10n.properties	Tue Apr 03 21:50:35 2018 +0200
+++ b/src/jdk.jshell/share/classes/jdk/internal/jshell/tool/resources/l10n.properties	Tue Apr 03 13:27:58 2018 -0700
@@ -163,6 +163,11 @@
 
 jshell.err.corrupted.stored.startup = Corrupted stored startup, using default -- {0}
 
+jshell.err.exception.thrown = Exception {0}
+jshell.err.exception.thrown.message = Exception {0}: {1}
+jshell.err.exception.cause = Caused by: {0}
+jshell.err.exception.cause.message = Caused by: {0}: {1}
+
 jshell.console.see.synopsis = <press tab again to see synopsis>
 jshell.console.see.full.documentation = <press tab again to see full documentation>
 jshell.console.see.documentation = <press tab again to see documentation>
--- a/src/jdk.jshell/share/classes/jdk/jshell/Eval.java	Tue Apr 03 21:50:35 2018 +0200
+++ b/src/jdk.jshell/share/classes/jdk/jshell/Eval.java	Tue Apr 03 13:27:58 2018 -0700
@@ -850,17 +850,15 @@
                             ? expunge(value)
                             : "";
                 } catch (ResolutionException ex) {
-                    DeclarationSnippet sn = (DeclarationSnippet) state.maps.getSnippetDeadOrAlive(ex.id());
-                    exception = new UnresolvedReferenceException(sn, translateExceptionStack(ex));
+                    exception = asUnresolvedReferenceException(ex);
                 } catch (UserException ex) {
-                    exception = new EvalException(ex.getMessage(),
-                            ex.causeExceptionClass(),
-                            translateExceptionStack(ex));
+                    exception = asEvalException(ex);
                 } catch (RunException ex) {
                     // StopException - no-op
                 } catch (InternalException ex) {
                     state.debug(ex, "invoke");
                 } catch (EngineTerminationException ex) {
+                    state.debug(ex, "termination");
                     state.closeDown();
                 }
             } else if (si.subKind() == SubKind.VAR_DECLARATION_SUBKIND) {
@@ -890,6 +888,36 @@
         return events(c, outs, value, exception);
     }
 
+    // Convert an internal UserException to an API EvalException, translating
+    // the stack to snippet form.  Convert any chained exceptions
+    private EvalException asEvalException(UserException ue) {
+        return new EvalException(ue.getMessage(),
+                ue.causeExceptionClass(),
+                translateExceptionStack(ue),
+                asJShellException(ue.getCause()));
+    }
+
+    // Convert an internal ResolutionException to an API UnresolvedReferenceException,
+    // translating the snippet id to snipper and the stack to snippet form
+    private UnresolvedReferenceException asUnresolvedReferenceException(ResolutionException re) {
+        DeclarationSnippet sn = (DeclarationSnippet) state.maps.getSnippetDeadOrAlive(re.id());
+        return new UnresolvedReferenceException(sn, translateExceptionStack(re));
+    }
+
+    // Convert an internal UserException/ResolutionException to an API
+    // EvalException/UnresolvedReferenceException
+    private JShellException asJShellException(Throwable e) {
+        if (e == null) {
+            return null;
+        } else if (e instanceof UserException) {
+            return asEvalException((UserException) e);
+        } else if (e instanceof ResolutionException) {
+            return asUnresolvedReferenceException((ResolutionException) e);
+        } else {
+            throw new AssertionError(e);
+        }
+    }
+
     private boolean interestingEvent(SnippetEvent e) {
         return e.isSignatureChange()
                     || e.causeSnippet() == null
--- a/src/jdk.jshell/share/classes/jdk/jshell/EvalException.java	Tue Apr 03 21:50:35 2018 +0200
+++ b/src/jdk.jshell/share/classes/jdk/jshell/EvalException.java	Tue Apr 03 13:27:58 2018 -0700
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2015, Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2015, 2018, Oracle and/or its affiliates. All rights reserved.
  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
  *
  * This code is free software; you can redistribute it and/or modify it
@@ -26,10 +26,10 @@
 package jdk.jshell;
 
 /**
- * Wraps an exception thrown in the remotely executing client.
+ * Wraps an throwable thrown in the executing client.
  * An instance of <code>EvalException</code> can be returned in the
  * {@link jdk.jshell.SnippetEvent#exception()} query.
- * The name of the exception thrown is available from
+ * The name of the throwable thrown is available from
  * {@link jdk.jshell.EvalException#getExceptionClassName()}.
  * Message and stack can be queried by methods on <code>Exception</code>.
  * <p>
@@ -45,8 +45,9 @@
 public class EvalException extends JShellException {
     private final String exceptionClass;
 
-    EvalException(String message, String exceptionClass, StackTraceElement[] stackElements) {
-        super(message);
+    EvalException(String message, String exceptionClass,
+            StackTraceElement[] stackElements, JShellException cause) {
+        super(message, cause);
         this.exceptionClass = exceptionClass;
         this.setStackTrace(stackElements);
     }
@@ -63,4 +64,18 @@
         return exceptionClass;
     }
 
+    /**
+     * Returns the wrapped cause of the throwable in the executing client
+     * represented by this {@code EvalException} or {@code null} if the cause is
+     * nonexistent or unknown.
+     *
+     * @return the cause wrapped in a {@code EvalException} or
+     * {@link UnresolvedReferenceException} or return {@code null} if the cause
+     * is nonexistent or unknown.
+     * @since 11
+     */
+    @Override
+    public JShellException getCause() {
+        return (JShellException) super.getCause();
+    }
 }
--- a/src/jdk.jshell/share/classes/jdk/jshell/JShellException.java	Tue Apr 03 21:50:35 2018 +0200
+++ b/src/jdk.jshell/share/classes/jdk/jshell/JShellException.java	Tue Apr 03 13:27:58 2018 -0700
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2016, Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2016, 2018, Oracle and/or its affiliates. All rights reserved.
  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
  *
  * This code is free software; you can redistribute it and/or modify it
@@ -36,4 +36,8 @@
     JShellException(String message) {
         super(message);
     }
+
+    JShellException(String message, Throwable cause) {
+        super(message, cause);
+    }
 }
--- a/src/jdk.jshell/share/classes/jdk/jshell/UnresolvedReferenceException.java	Tue Apr 03 21:50:35 2018 +0200
+++ b/src/jdk.jshell/share/classes/jdk/jshell/UnresolvedReferenceException.java	Tue Apr 03 13:27:58 2018 -0700
@@ -45,7 +45,7 @@
     final DeclarationSnippet snippet;
 
     UnresolvedReferenceException(DeclarationSnippet snippet, StackTraceElement[] stackElements) {
-        super("Attempt to use definition snippet with unresolved references");
+        super("Attempt to use definition snippet with unresolved references in " + snippet);
         this.snippet = snippet;
         this.setStackTrace(stackElements);
     }
--- a/src/jdk.jshell/share/classes/jdk/jshell/execution/DirectExecutionControl.java	Tue Apr 03 21:50:35 2018 +0200
+++ b/src/jdk.jshell/share/classes/jdk/jshell/execution/DirectExecutionControl.java	Tue Apr 03 13:27:58 2018 -0700
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2016, 2017, Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2016, 2018, Oracle and/or its affiliates. All rights reserved.
  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
  *
  * This code is free software; you can redistribute it and/or modify it
@@ -288,11 +288,20 @@
      * @throws ExecutionControl.InternalException for internal problems
      */
     protected String throwConvertedInvocationException(Throwable cause) throws RunException, InternalException {
-        if (cause instanceof SPIResolutionException) {
-            SPIResolutionException spire = (SPIResolutionException) cause;
-            throw new ResolutionException(spire.id(), spire.getStackTrace());
+        throw asRunException(cause);
+    }
+
+    private RunException asRunException(Throwable ex) {
+        if (ex instanceof SPIResolutionException) {
+            SPIResolutionException spire = (SPIResolutionException) ex;
+            return new ResolutionException(spire.id(), spire.getStackTrace());
         } else {
-            throw new UserException(cause.getMessage(), cause.getClass().getName(), cause.getStackTrace());
+            UserException ue = new UserException(ex.getMessage(),
+                    ex.getClass().getName(),
+                    ex.getStackTrace());
+            Throwable cause = ex.getCause();
+            ue.initCause(cause == null ? null : asRunException(cause));
+            return ue;
         }
     }
 
--- a/src/jdk.jshell/share/classes/jdk/jshell/execution/ExecutionControlForwarder.java	Tue Apr 03 21:50:35 2018 +0200
+++ b/src/jdk.jshell/share/classes/jdk/jshell/execution/ExecutionControlForwarder.java	Tue Apr 03 13:27:58 2018 -0700
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2016, Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2016, 2018, Oracle and/or its affiliates. All rights reserved.
  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
  *
  * This code is free software; you can redistribute it and/or modify it
@@ -195,8 +195,7 @@
             flush();
             return true;
         } catch (InternalException ex) {
-            writeStatus(RESULT_INTERNAL_PROBLEM);
-            writeUTF(ex.getMessage());
+            writeInternalException(ex);
             flush();
             return true;
         } catch (ClassInstallException ex) {
@@ -206,16 +205,24 @@
             flush();
             return true;
         } catch (UserException ex) {
-            writeStatus(RESULT_USER_EXCEPTION);
-            writeNullOrUTF(ex.getMessage());
-            writeUTF(ex.causeExceptionClass());
-            writeObject(ex.getStackTrace());
+            writeStatus(RESULT_USER_EXCEPTION_CHAINED);
+            for (Throwable e = ex; e != null; ) {
+                if (e instanceof UserException) {
+                    writeUserException((UserException) e);
+                    e = e.getCause();
+                } else if (e instanceof ResolutionException) {
+                    writeResolutionException((ResolutionException) e);
+                    e = null;
+                } else {
+                    writeInternalException(e);
+                    e = null;
+                }
+            }
+            writeStatus(RESULT_SUCCESS);
             flush();
             return true;
         } catch (ResolutionException ex) {
-            writeStatus(RESULT_CORRALLED);
-            writeInt(ex.id());
-            writeObject(ex.getStackTrace());
+            writeResolutionException(ex);
             flush();
             return true;
         } catch (StoppedException ex) {
@@ -232,6 +239,24 @@
         }
     }
 
+    void writeInternalException(Throwable ex) throws IOException {
+        writeStatus(RESULT_INTERNAL_PROBLEM);
+        writeUTF(ex.getMessage());
+    }
+
+    void writeUserException(UserException ex) throws IOException {
+        writeStatus(RESULT_USER_EXCEPTION);
+        writeNullOrUTF(ex.getMessage());
+        writeUTF(ex.causeExceptionClass());
+        writeObject(ex.getStackTrace());
+    }
+
+    void writeResolutionException(ResolutionException ex) throws IOException {
+        writeStatus(RESULT_CORRALLED);
+        writeInt(ex.id());
+        writeObject(ex.getStackTrace());
+    }
+
     void commandLoop() {
         try {
             while (processCommand()) {
--- a/src/jdk.jshell/share/classes/jdk/jshell/execution/RemoteCodes.java	Tue Apr 03 21:50:35 2018 +0200
+++ b/src/jdk.jshell/share/classes/jdk/jshell/execution/RemoteCodes.java	Tue Apr 03 13:27:58 2018 -0700
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2014, 2016, Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2014, 2018, Oracle and/or its affiliates. All rights reserved.
  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
  *
  * This code is free software; you can redistribute it and/or modify it
@@ -89,7 +89,7 @@
      */
     static final int RESULT_INTERNAL_PROBLEM        = 103;
     /**
-     * User exception encountered.
+     * User exception encountered. Legacy and used within RESULT_USER_EXCEPTION_CHAINED
      */
     static final int RESULT_USER_EXCEPTION          = 104;
     /**
@@ -104,5 +104,9 @@
      * The invoke has been stopped.
      */
     static final int RESULT_STOPPED                 = 107;
-
+    /**
+     * User exception encountered.
+     * @since 11
+     */
+    static final int RESULT_USER_EXCEPTION_CHAINED  = 108;
 }
--- a/src/jdk.jshell/share/classes/jdk/jshell/execution/StreamingExecutionControl.java	Tue Apr 03 21:50:35 2018 +0200
+++ b/src/jdk.jshell/share/classes/jdk/jshell/execution/StreamingExecutionControl.java	Tue Apr 03 13:27:58 2018 -0700
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2016, Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2016, 2018, Oracle and/or its affiliates. All rights reserved.
  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
  *
  * This code is free software; you can redistribute it and/or modify it
@@ -283,18 +283,46 @@
                     throw new NotImplementedException(message);
                 }
                 case RESULT_USER_EXCEPTION: {
-                    // A user exception was encountered.
-                    String message = readNullOrUTF();
-                    String exceptionClassName = in.readUTF();
-                    StackTraceElement[] elems = (StackTraceElement[]) in.readObject();
-                    throw new UserException(message, exceptionClassName, elems);
+                    // A user exception was encountered.  Handle pre JDK 11 back-ends
+                    throw readUserException();
                 }
                 case RESULT_CORRALLED: {
                     // An unresolved reference was encountered.
-                    int id = in.readInt();
-                    StackTraceElement[] elems = (StackTraceElement[]) in.readObject();
-                    ResolutionException re = new ResolutionException(id, elems);
-                    throw re;
+                    throw readResolutionException();
+                }
+                case RESULT_USER_EXCEPTION_CHAINED: {
+                    // A user exception was encountered -- transmit chained.
+                    in.readInt(); // always RESULT_USER_EXCEPTION
+                    UserException result = readUserException();
+                    RunException caused = result;
+                    // Loop through the chained causes (if any) building a chained exception
+                    loop: while (true) {
+                        RunException ex;
+                        int cstatus = in.readInt();
+                        switch (cstatus) {
+                            case RESULT_USER_EXCEPTION: {
+                                // A user exception was the proximal cause.
+                                ex = readUserException();
+                                break;
+                            }
+                            case RESULT_CORRALLED: {
+                                // An unresolved reference was the underlying cause.
+                                ex = readResolutionException();
+                                break;
+                            }
+                            case RESULT_SUCCESS: {
+                                // End of chained exceptions
+                                break loop;
+                            }
+                            default: {
+                                throw new EngineTerminationException("Bad chained remote result code: " + cstatus);
+                            }
+                        }
+                        caused.initCause(ex);
+                        caused = ex;
+                    }
+                    caused.initCause(null); // root cause has no cause
+                    throw result;
                 }
                 case RESULT_STOPPED: {
                     // Execution was aborted by the stop()
@@ -314,8 +342,21 @@
                 }
             }
         } catch (IOException | ClassNotFoundException ex) {
+            ex.printStackTrace();
             throw new EngineTerminationException(ex.toString());
         }
     }
 
+    private UserException readUserException() throws IOException, ClassNotFoundException {
+        String message = readNullOrUTF();
+        String exceptionClassName = in.readUTF();
+        StackTraceElement[] elems = (StackTraceElement[]) in.readObject();
+        return new UserException(message, exceptionClassName, elems);
+    }
+
+    private ResolutionException readResolutionException() throws IOException, ClassNotFoundException {
+        int id = in.readInt();
+        StackTraceElement[] elems = (StackTraceElement[]) in.readObject();
+        return new ResolutionException(id, elems);
+    }
 }
--- a/test/langtools/jdk/jshell/ExceptionsTest.java	Tue Apr 03 21:50:35 2018 +0200
+++ b/test/langtools/jdk/jshell/ExceptionsTest.java	Tue Apr 03 13:27:58 2018 -0700
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2015, Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2015, 2018, Oracle and/or its affiliates. All rights reserved.
  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
  *
  * This code is free software; you can redistribute it and/or modify it
@@ -24,16 +24,20 @@
 /*
  * @test
  * @summary Tests for exceptions
+ * @bug 8198801
  * @build KullaTesting TestingInputStream
  * @run testng ExceptionsTest
  */
 
-import jdk.jshell.SnippetEvent;
-import jdk.jshell.EvalException;
+import java.io.IOException;
 import java.io.PrintWriter;
 import java.io.StringWriter;
+import jdk.jshell.EvalException;
+import jdk.jshell.JShellException;
+import jdk.jshell.Snippet;
+import jdk.jshell.SnippetEvent;
+import jdk.jshell.UnresolvedReferenceException;
 
-import jdk.jshell.Snippet;
 import org.testng.annotations.Test;
 
 import static org.testng.Assert.*;
@@ -81,6 +85,54 @@
                         newStackTraceElement("", "", cr3.snippet(), 1)));
     }
 
+    public void throwChained() {
+        String message1 = "error_message1";
+        String message2 = "error_message2";
+        Snippet s1 = methodKey(assertEval("void p() throws Exception { ((String) null).toString(); }"));
+        Snippet s2 = methodKey(assertEval("void n() throws Exception { try { p(); } catch (Exception ex) { throw new java.io.IOException(\"" + message2 + "\", ex); }}"));
+        Snippet s3 = methodKey(assertEval("void m() {\n"
+                + "try { n(); }\n"
+                + "catch (Exception ex) {\n"
+                + "    throw new RuntimeException(\"" + message1 + "\", ex);\n"
+                + "}}"));
+        SnippetEvent cr4 = assertEvalException("m();");
+        assertExceptionMatch(cr4,
+                new ExceptionInfo(RuntimeException.class, message1,
+                        new ExceptionInfo(IOException.class, message2,
+                                new ExceptionInfo(NullPointerException.class, null,
+                                        newStackTraceElement("", "p", s1, 1),
+                                        newStackTraceElement("", "n", s2, 1),
+                                        newStackTraceElement("", "m", s3, 2),
+                                        newStackTraceElement("", "", cr4.snippet(), 1)),
+                                newStackTraceElement("", "n", s2, 1),
+                                newStackTraceElement("", "m", s3, 2),
+                                newStackTraceElement("", "", cr4.snippet(), 1)),
+                        newStackTraceElement("", "m", s3, 4),
+                        newStackTraceElement("", "", cr4.snippet(), 1)));
+    }
+
+    public void throwChainedUnresolved() {
+        String message1 = "error_message1";
+        String message2 = "error_message2";
+        Snippet s1 = methodKey(assertEval("void p() throws Exception { ((String) null).toString(); }"));
+        Snippet s2 = methodKey(assertEval("void n() throws Exception { try { p(); } catch (Exception ex) { throw new java.io.IOException(\"" + message2 + "\", ex); }}"));
+        Snippet s3 = methodKey(assertEval("void m() {\n"
+                + "try { n(); }\n"
+                + "catch (Exception ex) {\n"
+                + "    throw new RuntimeException(\"" + message1 + "\", ex);\n"
+                + "}}"));
+        getState().drop(s1);
+        SnippetEvent cr4 = assertEvalException("m();");
+        assertExceptionMatch(cr4,
+                new ExceptionInfo(RuntimeException.class, message1,
+                        new UnresolvedExceptionInfo(s2,
+                                newStackTraceElement("", "n", s2, 1),
+                                newStackTraceElement("", "m", s3, 2),
+                                newStackTraceElement("", "", cr4.snippet(), 1)),
+                        newStackTraceElement("", "m", s3, 4),
+                        newStackTraceElement("", "", cr4.snippet(), 1)));
+    }
+
     public void throwFromConstructor() {
         String message = "error_message";
         Snippet s1 = methodKey(assertEval("void f() { throw new RuntimeException(\"" + message + "\"); }"));
@@ -171,15 +223,42 @@
         return new StackTraceElement(className, methodName, "#" + key.id(), lineNumber);
     }
 
-    private static class ExceptionInfo {
-        public final Class<? extends Throwable> exception;
-        public final String message;
+    private static class AnyExceptionInfo {
+
         public final StackTraceElement[] stackTraceElements;
 
-        public ExceptionInfo(Class<? extends Throwable> exception, String message, StackTraceElement...stackTraceElements) {
+        public AnyExceptionInfo(StackTraceElement... stackTraceElements) {
+            this.stackTraceElements = stackTraceElements.length == 0 ? null : stackTraceElements;
+        }
+    }
+
+    private static class UnresolvedExceptionInfo extends AnyExceptionInfo {
+
+        public final Snippet sn;
+
+        public UnresolvedExceptionInfo(Snippet sn, StackTraceElement... stackTraceElements) {
+            super(stackTraceElements);
+            this.sn = sn;
+        }
+    }
+
+    private static class ExceptionInfo extends AnyExceptionInfo {
+
+        public final Class<? extends Throwable> exception;
+        public final String message;
+        public final AnyExceptionInfo cause;
+
+        public ExceptionInfo(Class<? extends Throwable> exception, String message,
+                StackTraceElement... stackTraceElements) {
+            this(exception, message, null, stackTraceElements);
+        }
+
+        public ExceptionInfo(Class<? extends Throwable> exception, String message,
+                AnyExceptionInfo cause, StackTraceElement... stackTraceElements) {
+            super(stackTraceElements);
             this.exception = exception;
             this.message = message;
-            this.stackTraceElements = stackTraceElements.length == 0 ? null : stackTraceElements;
+            this.cause = cause;
         }
     }
 
@@ -188,28 +267,51 @@
     }
 
     private void assertExceptionMatch(SnippetEvent cr, ExceptionInfo exceptionInfo) {
-        assertNotNull(cr.exception(), "Expected exception was not thrown: " + exceptionInfo.exception);
-        if (cr.exception() instanceof EvalException) {
-            EvalException ex = (EvalException) cr.exception();
+        assertExceptionMatch(cr.exception(), cr.snippet().source(), exceptionInfo);
+    }
+
+    private void assertExceptionMatch(Throwable exception, String source, ExceptionInfo exceptionInfo) {
+        assertNotNull(exception, "Expected exception was not thrown: " + exceptionInfo.exception);
+        if (exception instanceof EvalException) {
+            EvalException ex = (EvalException) exception;
             String actualException = ex.getExceptionClassName();
             String expectedException = exceptionInfo.exception.getCanonicalName();
-            String stackTrace = getStackTrace(ex);
-            String source = cr.snippet().source();
             assertEquals(actualException, expectedException,
                     String.format("Given \"%s\" expected exception: %s, got: %s%nStack trace:%n%s",
-                            source, expectedException, actualException, stackTrace));
+                            source, expectedException, actualException, getStackTrace(ex)));
             if (exceptionInfo.message != null) {
                 assertEquals(ex.getMessage(), exceptionInfo.message,
                         String.format("Given \"%s\" expected message: %s, got: %s",
                                 source, exceptionInfo.message, ex.getMessage()));
             }
-            if (exceptionInfo.stackTraceElements != null) {
-                assertStackTrace(ex.getStackTrace(), exceptionInfo.stackTraceElements,
-                        String.format("Given \"%s\"%nStack trace:%n%s%n",
-                                source, stackTrace));
+            assertStackMatch(ex, source, exceptionInfo);
+            if (exceptionInfo.cause != null) {
+                assertAnyExceptionMatch(exception.getCause(), exceptionInfo.cause);
             }
         } else {
-            fail("Unexpected execution exceptionInfo: " + cr.exception());
+            fail("Unexpected exception: " + exception + " or exceptionInfo: " + exceptionInfo);
+        }
+    }
+
+    private void assertStackMatch(JShellException exception, String source, AnyExceptionInfo exceptionInfo) {
+        if (exceptionInfo.stackTraceElements != null) {
+            assertStackTrace(exception.getStackTrace(), exceptionInfo.stackTraceElements,
+                    String.format("Given \"%s\"%nStack trace:%n%s%n",
+                            source, getStackTrace(exception)));
+        }
+    }
+
+    private void assertAnyExceptionMatch(Throwable exception, AnyExceptionInfo exceptionInfo) {
+        if (exceptionInfo instanceof ExceptionInfo) {
+            assertExceptionMatch(exception, "", (ExceptionInfo) exceptionInfo);
+        } else {
+            assertTrue(exceptionInfo instanceof UnresolvedExceptionInfo, "Bad exceptionInfo: " + exceptionInfo);
+            assertTrue(exception instanceof UnresolvedReferenceException,
+                    "Expected UnresolvedReferenceException: " + exception);
+            UnresolvedExceptionInfo uei = (UnresolvedExceptionInfo) exceptionInfo;
+            UnresolvedReferenceException ure = (UnresolvedReferenceException) exception;
+            assertEquals(ure.getSnippet(), uei.sn);
+            assertStackMatch(ure, "", exceptionInfo);
         }
     }
 
@@ -236,7 +338,7 @@
         }
     }
 
-    private String getStackTrace(EvalException ex) {
+    private String getStackTrace(Throwable ex) {
         StringWriter st = new StringWriter();
         ex.printStackTrace(new PrintWriter(st));
         return st.toString();
--- a/test/langtools/jdk/jshell/ToolSimpleTest.java	Tue Apr 03 21:50:35 2018 +0200
+++ b/test/langtools/jdk/jshell/ToolSimpleTest.java	Tue Apr 03 13:27:58 2018 -0700
@@ -23,7 +23,7 @@
 
 /*
  * @test
- * @bug 8153716 8143955 8151754 8150382 8153920 8156910 8131024 8160089 8153897 8167128 8154513 8170015 8170368 8172102 8172103  8165405 8173073 8173848 8174041 8173916 8174028 8174262 8174797 8177079 8180508 8177466 8172154 8192979 8191842 8198573
+ * @bug 8153716 8143955 8151754 8150382 8153920 8156910 8131024 8160089 8153897 8167128 8154513 8170015 8170368 8172102 8172103  8165405 8173073 8173848 8174041 8173916 8174028 8174262 8174797 8177079 8180508 8177466 8172154 8192979 8191842 8198573 8198801
  * @summary Simple jshell tool tests
  * @modules jdk.compiler/com.sun.tools.javac.api
  *          jdk.compiler/com.sun.tools.javac.main
@@ -87,6 +87,32 @@
     }
 
     @Test
+    public void testChainedThrow() {
+        test(
+                (a) -> assertCommand(a, "void p() throws Exception { ((String) null).toString(); }",
+                        "|  created method p()"),
+                (a) -> assertCommand(a, "void n() throws Exception { try { p(); } catch (Exception ex) { throw new IOException(\"bar\", ex); }}",
+                        "|  created method n()"),
+                (a) -> assertCommand(a, "void m() { try { n(); } catch (Exception ex) { throw new RuntimeException(\"foo\", ex); }}",
+                        "|  created method m()"),
+                (a) -> assertCommand(a, "m()",
+                          "|  Exception java.lang.RuntimeException: foo\n"
+                        + "|        at m (#3:1)\n"
+                        + "|        at (#4:1)\n"
+                        + "|  Caused by: java.io.IOException: bar\n"
+                        + "|        at n (#2:1)\n"
+                        + "|        ...\n"
+                        + "|  Caused by: java.lang.NullPointerException\n"
+                        + "|        at p (#1:1)\n"
+                        + "|        ..."),
+                (a) -> assertCommand(a, "/drop p",
+                        "|  dropped method p()"),
+                (a) -> assertCommand(a, "m()",
+                        "|  attempted to call method n() which cannot be invoked until method p() is declared")
+        );
+    }
+
+    @Test
     public void oneLineOfError() {
         test(
                 (a) -> assertCommand(a, "12+", null),