This is an automated email from the ASF dual-hosted git repository.

ptupitsyn pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/ignite-3.git


The following commit(s) were added to refs/heads/main by this push:
     new 2c9fce5bdfa IGNITE-28000 Reduce verbosity of client stacktraces (#7854)
2c9fce5bdfa is described below

commit 2c9fce5bdfa68f809051aa5b4c8cb19850a3d805
Author: Tiago Marques Godinho <[email protected]>
AuthorDate: Tue Apr 21 17:11:42 2026 +0100

    IGNITE-28000 Reduce verbosity of client stacktraces (#7854)
    
    - Simplify exceptions returns by TcpClientChannel#readError
    - Remove unnecessary calls to ensurePublicException
    - Improve TraceableExceptionMapper
    - Add copy method to some exceptions with fields.
    - Update ViewUtils ensurePublicException implementation
    - ClientTable now is responsible for ensuringPublicExceptions
    - Refactor error handling in ClientSQL
    - Reuse ViewUtils#sync in ClientCompute methods
    - Update SQL Exception Mapper
    - Do not wrap MarshallerException in MarshallerException in KeyValueViewImpl
    - Update ThinClientTests error checks
    - Update ItComputeTest error handling checks
    - Update ConnectionTest error check
    - Update error handling checks in tests
    - Update ItThinClientTransactionTest
    - Add CancelationException to ViewUtils#sync
---
 .../org/apache/ignite/sql/SqlBatchException.java   |  11 ++
 .../exception/handler/SqlExceptionHandler.java     |  20 +-
 .../internal/client/ItThinClientComputeTest.java   |  82 ++++-----
 ...tThinClientComputeTypeCheckMarshallingTest.java |  30 ++-
 .../client/ItThinClientConnectionTest.java         |  12 +-
 .../client/ItThinClientTransactionsTest.java       |  37 +++-
 .../client/ClientExceptionMapperProvider.java      |  63 +++++++
 .../ClientRetriableTransactionException.java       |   8 +-
 .../ignite/internal/client/TcpClientChannel.java   |  56 ++++--
 .../internal/client/compute/ClientCompute.java     |  10 +-
 .../ignite/internal/client/sql/ClientSql.java      |  48 +----
 .../ignite/internal/client/table/ClientTable.java  |  10 +-
 .../tx/ClientTransactionKilledException.java       |   4 +-
 .../org/apache/ignite/client/ConnectionTest.java   |   2 +-
 .../ignite/internal/compute/ItComputeBaseTest.java |  97 ++++++----
 .../internal/compute/ItComputeStandaloneTest.java  |   5 +
 .../internal/compute/ItComputeTestClient.java      |   5 +
 .../internal/compute/ItComputeTestEmbedded.java    |   7 +-
 .../org/apache/ignite/internal/util/ViewUtils.java |  66 +++++--
 .../apache/ignite/internal/util/ViewUtilsTest.java |  11 +-
 .../ignite/internal/IgniteExceptionTestUtils.java  | 203 +++++++++++++++++++++
 .../ignite/internal/TraceableExceptionMatcher.java |  11 +-
 .../org/apache/ignite/internal/ssl/ItSslTest.java  |  17 +-
 .../sql/engine/ItPkOnlyTableCrossApiTest.java      |   9 +-
 .../ignite/internal/table/KeyValueViewImpl.java    |   6 +-
 .../ignite/internal/tx/ItRunInTransactionTest.java |   9 +-
 26 files changed, 626 insertions(+), 213 deletions(-)

diff --git 
a/modules/api/src/main/java/org/apache/ignite/sql/SqlBatchException.java 
b/modules/api/src/main/java/org/apache/ignite/sql/SqlBatchException.java
index 13613b5c262..226666f541f 100644
--- a/modules/api/src/main/java/org/apache/ignite/sql/SqlBatchException.java
+++ b/modules/api/src/main/java/org/apache/ignite/sql/SqlBatchException.java
@@ -77,4 +77,15 @@ public class SqlBatchException extends SqlException {
     public long[] updateCounters() {
         return updCntrs;
     }
+
+    /**
+     * Copy the exception.
+     *
+     * @param src Exception to copy.
+     * @return new copied exception.
+     */
+    @SuppressWarnings("PMD.UnusedPrivateMethod")
+    private static SqlBatchException copy(SqlBatchException src) {
+        return new SqlBatchException(src.traceId(), src.code(), 
src.updateCounters(), src.getMessage(), src.getCause());
+    }
 }
diff --git 
a/modules/cli/src/main/java/org/apache/ignite/internal/cli/core/exception/handler/SqlExceptionHandler.java
 
b/modules/cli/src/main/java/org/apache/ignite/internal/cli/core/exception/handler/SqlExceptionHandler.java
index 5419f91043d..78bbf6eaa8b 100644
--- 
a/modules/cli/src/main/java/org/apache/ignite/internal/cli/core/exception/handler/SqlExceptionHandler.java
+++ 
b/modules/cli/src/main/java/org/apache/ignite/internal/cli/core/exception/handler/SqlExceptionHandler.java
@@ -65,8 +65,8 @@ public class SqlExceptionHandler implements 
ExceptionHandler<SQLException> {
             return fromIgniteException("Client error", e);
         }
 
-        if (e.getCause() instanceof IgniteClientConnectionException) {
-            IgniteClientConnectionException cause = 
(IgniteClientConnectionException) e.getCause();
+        if (e instanceof IgniteClientConnectionException) {
+            IgniteClientConnectionException cause = 
(IgniteClientConnectionException) e;
 
             SSLHandshakeException sslHandshakeException = findCause(cause, 
SSLHandshakeException.class);
             if (sslHandshakeException != null) {
@@ -86,9 +86,23 @@ public class SqlExceptionHandler implements 
ExceptionHandler<SQLException> {
     private static ErrorUiComponent authnErrUiComponent(IgniteException e) {
         InvalidCredentialsException invalidCredentialsException = findCause(e, 
InvalidCredentialsException.class);
         if (invalidCredentialsException != null) {
+            String msg = invalidCredentialsException.getMessage();
+
+            String details = msg;
+            if (msg != null) {
+                var headerIdx = msg.indexOf('\n');
+                if (headerIdx != -1) {
+                    details = msg.substring(0, headerIdx);
+                    int traceInfoIdx = details.indexOf(" TraceId:");
+                    if (traceInfoIdx != -1) {
+                        details = details.substring(0, traceInfoIdx);
+                    }
+                }
+            }
+
             return ErrorUiComponent.builder()
                     .header("Could not connect to node. Check authentication 
configuration")
-                    .details(invalidCredentialsException.getMessage())
+                    .details(details)
                     .verbose(extractCauseMessage(e.getMessage()))
                     .build();
         }
diff --git 
a/modules/client/src/integrationTest/java/org/apache/ignite/internal/client/ItThinClientComputeTest.java
 
b/modules/client/src/integrationTest/java/org/apache/ignite/internal/client/ItThinClientComputeTest.java
index 0677b1f18c5..53cc880bb73 100644
--- 
a/modules/client/src/integrationTest/java/org/apache/ignite/internal/client/ItThinClientComputeTest.java
+++ 
b/modules/client/src/integrationTest/java/org/apache/ignite/internal/client/ItThinClientComputeTest.java
@@ -23,7 +23,8 @@ import static org.apache.ignite.compute.JobStatus.COMPLETED;
 import static org.apache.ignite.compute.JobStatus.EXECUTING;
 import static org.apache.ignite.compute.JobStatus.FAILED;
 import static org.apache.ignite.compute.JobStatus.QUEUED;
-import static 
org.apache.ignite.internal.IgniteExceptionTestUtils.traceableException;
+import static 
org.apache.ignite.internal.IgniteExceptionTestUtils.publicException;
+import static 
org.apache.ignite.internal.IgniteExceptionTestUtils.publicExceptionWithHint;
 import static 
org.apache.ignite.internal.testframework.matchers.CompletableFutureExceptionMatcher.willThrow;
 import static 
org.apache.ignite.internal.testframework.matchers.CompletableFutureExceptionMatcher.willThrowFast;
 import static 
org.apache.ignite.internal.testframework.matchers.CompletableFutureMatcher.will;
@@ -49,7 +50,6 @@ import static org.hamcrest.Matchers.not;
 import static org.hamcrest.Matchers.oneOf;
 import static org.junit.jupiter.api.Assertions.assertArrayEquals;
 import static org.junit.jupiter.api.Assertions.assertEquals;
-import static org.junit.jupiter.api.Assertions.assertInstanceOf;
 import static org.junit.jupiter.api.Assertions.assertNotNull;
 import static org.junit.jupiter.api.Assertions.assertThrows;
 import static org.junit.jupiter.api.Assertions.assertTrue;
@@ -93,6 +93,7 @@ import org.apache.ignite.compute.task.MapReduceTask;
 import org.apache.ignite.compute.task.TaskExecution;
 import org.apache.ignite.compute.task.TaskExecutionContext;
 import org.apache.ignite.deployment.DeploymentUnit;
+import org.apache.ignite.internal.IgniteExceptionTestUtils.Cause;
 import org.apache.ignite.internal.compute.JobTaskStatusMapper;
 import org.apache.ignite.internal.runner.app.Jobs;
 import org.apache.ignite.internal.testframework.IgniteTestUtils;
@@ -438,14 +439,10 @@ public class ItThinClientComputeTest extends 
ItAbstractThinClientTest {
                 submit(JobTarget.node(node(0)), 
JobDescriptor.builder(Jobs.IgniteExceptionJob.class).build(), null)
         );
 
-        assertThat(cause.getMessage(), containsString("Custom job error"));
-        assertEquals(Jobs.TRACE_ID, cause.traceId());
-        assertEquals(COLUMN_NOT_FOUND_ERR, cause.code());
-        assertInstanceOf(Jobs.CustomException.class, cause);
-        assertNotNull(cause.getCause());
-        String hint = cause.getCause().getMessage();
-
-        assertEquals("To see the full stack trace, set 
clientConnector.sendServerExceptionStackTraceToClient:true on the server", 
hint);
+        assertThat(cause,
+                publicExceptionWithHint(Jobs.CustomException.class, 
COLUMN_NOT_FOUND_ERR, "Custom job error")
+                    .withTraceId(is(Jobs.TRACE_ID))
+        );
     }
 
     @Test
@@ -455,14 +452,10 @@ public class ItThinClientComputeTest extends 
ItAbstractThinClientTest {
                         .execute(JobTarget.node(node(0)), 
JobDescriptor.builder(Jobs.IgniteExceptionJob.class).build(), null)
         );
 
-        assertThat(cause.getMessage(), containsString("Custom job error"));
-        assertEquals(Jobs.TRACE_ID, cause.traceId());
-        assertEquals(COLUMN_NOT_FOUND_ERR, cause.code());
-        assertInstanceOf(Jobs.CustomException.class, cause);
-        assertNotNull(cause.getCause());
-        String hint = cause.getCause().getMessage();
-
-        assertEquals("To see the full stack trace, set 
clientConnector.sendServerExceptionStackTraceToClient:true on the server", 
hint);
+        assertThat(cause,
+                publicExceptionWithHint(Jobs.CustomException.class, 
COLUMN_NOT_FOUND_ERR, "Custom job error")
+                        .withTraceId(is(Jobs.TRACE_ID))
+        );
     }
 
     @ParameterizedTest
@@ -654,31 +647,31 @@ public class ItThinClientComputeTest extends 
ItAbstractThinClientTest {
     }
 
     private static IgniteException 
getExceptionInJobExecutionSync(Supplier<String> execution) {
-        IgniteException ex = assertThrows(IgniteException.class, 
execution::get);
-
-        return (IgniteException) ex.getCause();
+        return assertThrows(IgniteException.class, execution::get);
     }
 
     private static void 
assertComputeExceptionWithClassAndMessage(IgniteException cause) {
-        String expectedMessage = "Job execution failed: 
java.lang.ArithmeticException: math err";
-        assertThat(cause, is(traceableException(ComputeException.class, 
COMPUTE_JOB_FAILED_ERR, expectedMessage)));
-
-        assertNotNull(cause.getCause());
-        String hint = cause.getCause().getMessage();
-
-        assertEquals("To see the full stack trace, set 
clientConnector.sendServerExceptionStackTraceToClient:true on the server", 
hint);
+        assertThat(cause,
+                publicExceptionWithHint(
+                        ComputeException.class,
+                        COMPUTE_JOB_FAILED_ERR,
+                        "Job execution failed: java.lang.ArithmeticException: 
math err"
+                )
+        );
     }
 
     private static void assertComputeExceptionWithStackTrace(IgniteException 
cause) {
-        String expectedMessage = "Job execution failed: 
java.lang.ArithmeticException: math err";
-        assertThat(cause, is(traceableException(ComputeException.class, 
COMPUTE_JOB_FAILED_ERR, expectedMessage)));
-
-        assertNotNull(cause.getCause());
-
-        assertThat(cause.getCause().getMessage(), containsString(
-                "Caused by: java.lang.ArithmeticException: math err" + 
System.lineSeparator()
-                        + "\tat 
org.apache.ignite.internal.client.ItThinClientComputeTest$"
-                        + 
"ExceptionJob.executeAsync(ItThinClientComputeTest.java:")
+        assertThat(cause,
+                publicException(
+                        ComputeException.class,
+                        COMPUTE_JOB_FAILED_ERR,
+                        "Job execution failed: java.lang.ArithmeticException: 
math err",
+                        List.of(
+                                Cause.of(ArithmeticException.class, "math err" 
+ System.lineSeparator()
+                                        + "\tat 
org.apache.ignite.internal.client.ItThinClientComputeTest$"
+                                        + 
"ExceptionJob.executeAsync(ItThinClientComputeTest.java:")
+                        )
+                )
         );
     }
 
@@ -891,14 +884,13 @@ public class ItThinClientComputeTest extends 
ItAbstractThinClientTest {
             TaskDescriptor<I, String> taskDescriptor = 
TaskDescriptor.builder(taskClass).build();
             IgniteException cause = 
getExceptionInTaskExecutionAsync(client.compute().submitMapReduce(taskDescriptor,
 null));
 
-            assertThat(cause.getMessage(), containsString("Custom job error"));
-            assertEquals(Jobs.TRACE_ID, cause.traceId());
-            assertEquals(COLUMN_NOT_FOUND_ERR, cause.code());
-            assertInstanceOf(Jobs.CustomException.class, cause);
-            assertNotNull(cause.getCause());
-            String hint = cause.getCause().getMessage();
-
-            assertEquals("To see the full stack trace, set 
clientConnector.sendServerExceptionStackTraceToClient:true on the server", 
hint);
+            assertThat(cause,
+                    publicExceptionWithHint(
+                            Jobs.CustomException.class,
+                            COLUMN_NOT_FOUND_ERR,
+                            "Custom job error"
+                    ).withTraceId(is(Jobs.TRACE_ID))
+            );
         }
     }
 
diff --git 
a/modules/client/src/integrationTest/java/org/apache/ignite/internal/client/ItThinClientComputeTypeCheckMarshallingTest.java
 
b/modules/client/src/integrationTest/java/org/apache/ignite/internal/client/ItThinClientComputeTypeCheckMarshallingTest.java
index 3463cc2e1d7..a6e1684485f 100644
--- 
a/modules/client/src/integrationTest/java/org/apache/ignite/internal/client/ItThinClientComputeTypeCheckMarshallingTest.java
+++ 
b/modules/client/src/integrationTest/java/org/apache/ignite/internal/client/ItThinClientComputeTypeCheckMarshallingTest.java
@@ -17,19 +17,20 @@
 
 package org.apache.ignite.internal.client;
 
+import static java.util.Collections.emptyList;
 import static java.util.concurrent.CompletableFuture.completedFuture;
 import static org.apache.ignite.compute.JobStatus.COMPLETED;
 import static org.apache.ignite.compute.JobStatus.FAILED;
-import static org.apache.ignite.internal.IgniteExceptionTestUtils.hasMessage;
+import static 
org.apache.ignite.internal.IgniteExceptionTestUtils.publicException;
 import static 
org.apache.ignite.internal.IgniteExceptionTestUtils.traceableException;
 import static 
org.apache.ignite.internal.testframework.matchers.CompletableFutureExceptionMatcher.willThrow;
 import static 
org.apache.ignite.internal.testframework.matchers.CompletableFutureMatcher.willBe;
 import static 
org.apache.ignite.internal.testframework.matchers.JobStateMatcher.jobStateWithStatus;
 import static org.awaitility.Awaitility.await;
 import static org.hamcrest.MatcherAssert.assertThat;
-import static org.hamcrest.Matchers.containsString;
 import static org.hamcrest.Matchers.instanceOf;
 
+import java.util.List;
 import java.util.concurrent.CompletableFuture;
 import org.apache.ignite.compute.ComputeException;
 import org.apache.ignite.compute.ComputeJob;
@@ -37,6 +38,7 @@ import org.apache.ignite.compute.JobDescriptor;
 import org.apache.ignite.compute.JobExecution;
 import org.apache.ignite.compute.JobExecutionContext;
 import org.apache.ignite.compute.JobTarget;
+import org.apache.ignite.internal.IgniteExceptionTestUtils.Cause;
 import org.apache.ignite.internal.runner.app.Jobs.ArgMarshallingJob;
 import org.apache.ignite.internal.runner.app.Jobs.ResultMarshallingJob;
 import org.apache.ignite.lang.ErrorGroups.Compute;
@@ -139,7 +141,7 @@ public class ItThinClientComputeTypeCheckMarshallingTest 
extends ItAbstractThinC
         assertResultFailsWithErr(
                 result, Compute.MARSHALLING_TYPE_MISMATCH_ERR,
                 "Exception in user-defined marshaller",
-                hasMessage(containsString("java.lang.RuntimeException: User 
defined error."))
+                List.of(Cause.of(RuntimeException.class, "User defined 
error."))
         );
     }
 
@@ -205,19 +207,35 @@ public class ItThinClientComputeTypeCheckMarshallingTest 
extends ItAbstractThinC
         }
     }
 
+    private static void assertResultFailsWithErr(
+            JobExecution<?> result,
+            int errCode,
+            String expectedMessage,
+            @Nullable Matcher<? extends Throwable> causeMatcher
+    ) {
+        assertThat(
+                result.resultAsync(),
+                willThrow(traceableException(ComputeException.class, errCode, 
expectedMessage).withCause(causeMatcher))
+        );
+    }
+
     private static void assertResultFailsWithErr(JobExecution<?> result, int 
errCode, String expectedMessage) {
-        assertResultFailsWithErr(result, errCode, expectedMessage, null);
+        assertResultFailsWithErr(result, errCode, expectedMessage, 
emptyList());
     }
 
     private static void assertResultFailsWithErr(
             JobExecution<?> result,
             int errCode,
             String expectedMessage,
-            @Nullable Matcher<? extends Throwable> causeMatcher
+            List<Cause> causes
     ) {
         assertThat(
                 result.resultAsync(),
-                willThrow(traceableException(ComputeException.class, errCode, 
expectedMessage).withCause(causeMatcher))
+                willThrow(
+                        publicException(
+                                ComputeException.class, errCode, 
expectedMessage, causes
+                        )
+                )
         );
     }
 }
diff --git 
a/modules/client/src/integrationTest/java/org/apache/ignite/internal/client/ItThinClientConnectionTest.java
 
b/modules/client/src/integrationTest/java/org/apache/ignite/internal/client/ItThinClientConnectionTest.java
index 99ffd6216fd..6cbfd0eec7f 100644
--- 
a/modules/client/src/integrationTest/java/org/apache/ignite/internal/client/ItThinClientConnectionTest.java
+++ 
b/modules/client/src/integrationTest/java/org/apache/ignite/internal/client/ItThinClientConnectionTest.java
@@ -17,6 +17,7 @@
 
 package org.apache.ignite.internal.client;
 
+import static 
org.apache.ignite.internal.IgniteExceptionTestUtils.publicExceptionWithHint;
 import static 
org.apache.ignite.internal.eventlog.api.IgniteEventType.CLIENT_CONNECTION_CLOSED;
 import static 
org.apache.ignite.internal.eventlog.api.IgniteEventType.CLIENT_CONNECTION_ESTABLISHED;
 import static org.apache.ignite.lang.ErrorGroups.Table.TABLE_NOT_FOUND_ERR;
@@ -37,9 +38,11 @@ import java.util.stream.IntStream;
 import org.apache.ignite.client.IgniteClient;
 import org.apache.ignite.internal.testframework.WorkDirectoryExtension;
 import org.apache.ignite.internal.testframework.log4j2.EventLogInspector;
+import org.apache.ignite.lang.ErrorGroups.Sql;
 import org.apache.ignite.lang.IgniteException;
 import org.apache.ignite.network.ClusterNode;
 import org.apache.ignite.sql.IgniteSql;
+import org.apache.ignite.sql.SqlException;
 import org.apache.ignite.table.RecordView;
 import org.apache.ignite.table.Table;
 import org.apache.ignite.table.Tuple;
@@ -126,17 +129,14 @@ public class ItThinClientConnectionTest extends 
ItAbstractThinClientTest {
     @Test
     void testExceptionHasHint() {
         // Execute on all nodes to collect all types of exception.
-        List<String> causes = IntStream.range(0, 
client().configuration().addresses().length)
+        List<IgniteException> causes = IntStream.range(0, 
client().configuration().addresses().length)
                 .mapToObj(i -> {
-                    IgniteException ex = assertThrows(IgniteException.class, 
() -> client().sql().execute("select x from bad"));
-
-                    return 
ex.getCause().getCause().getCause().getCause().getMessage();
+                    return assertThrows(IgniteException.class, () -> 
client().sql().execute("select x from bad"));
                 })
                 .collect(Collectors.toList());
 
         assertThat(causes,
-                hasItem(containsString("To see the full stack trace, "
-                        + "set 
clientConnector.sendServerExceptionStackTraceToClient:true on the server")));
+                hasItem(publicExceptionWithHint(SqlException.class, 
Sql.STMT_VALIDATION_ERR, "Object 'BAD' not found")));
     }
 
     @Test
diff --git 
a/modules/client/src/integrationTest/java/org/apache/ignite/internal/client/ItThinClientTransactionsTest.java
 
b/modules/client/src/integrationTest/java/org/apache/ignite/internal/client/ItThinClientTransactionsTest.java
index b5176e9a517..7d18e4b3478 100644
--- 
a/modules/client/src/integrationTest/java/org/apache/ignite/internal/client/ItThinClientTransactionsTest.java
+++ 
b/modules/client/src/integrationTest/java/org/apache/ignite/internal/client/ItThinClientTransactionsTest.java
@@ -20,12 +20,14 @@ package org.apache.ignite.internal.client;
 import static java.lang.String.format;
 import static java.util.Collections.emptyList;
 import static java.util.Comparator.comparing;
+import static 
org.apache.ignite.internal.IgniteExceptionTestUtils.publicException;
+import static 
org.apache.ignite.internal.IgniteExceptionTestUtils.publicExceptionWithHint;
 import static org.apache.ignite.internal.TestWrappers.unwrapIgniteImpl;
 import static 
org.apache.ignite.internal.testframework.matchers.CompletableFutureExceptionMatcher.willThrowWithCauseOrSuppressed;
 import static 
org.apache.ignite.internal.testframework.matchers.CompletableFutureMatcher.willSucceedFast;
 import static org.awaitility.Awaitility.await;
 import static org.hamcrest.MatcherAssert.assertThat;
-import static org.hamcrest.Matchers.containsString;
+import static org.hamcrest.Matchers.isA;
 import static org.hamcrest.Matchers.startsWith;
 import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
 import static org.junit.jupiter.api.Assertions.assertEquals;
@@ -70,6 +72,7 @@ import org.apache.ignite.internal.tx.Lock;
 import org.apache.ignite.internal.tx.TxState;
 import org.apache.ignite.internal.util.CollectionUtils;
 import org.apache.ignite.lang.ErrorGroups;
+import org.apache.ignite.lang.ErrorGroups.Common;
 import org.apache.ignite.lang.ErrorGroups.Transactions;
 import org.apache.ignite.lang.IgniteException;
 import org.apache.ignite.network.ClusterNode;
@@ -322,11 +325,14 @@ public class ItThinClientTransactionsTest extends 
ItAbstractThinClientTest {
         };
 
         var ex = assertThrows(IgniteException.class, () -> kvView().put(tx, 1, 
"1"));
-
-        String expected = "Unsupported transaction implementation: "
-                + "'class 
org.apache.ignite.internal.client.ItThinClientTransactionsTest";
-
-        assertThat(ex.getMessage(), containsString(expected));
+        assertThat(ex,
+                publicException(
+                    IgniteException.class,
+                    Common.INTERNAL_ERR,
+                    format("Unsupported transaction implementation: 'class 
%s'", tx.getClass().getName()),
+                    emptyList()
+                )
+        );
     }
 
     @Test
@@ -338,8 +344,14 @@ public class ItThinClientTransactionsTest extends 
ItAbstractThinClientTest {
             RecordView<Tuple> recordView = 
client2.tables().tables().get(0).recordView();
 
             var ex = assertThrows(IgniteException.class, () -> 
recordView.upsert(tx, Tuple.create()));
-
-            assertThat(ex.getMessage(), containsString("Transaction belongs to 
a different client instance"));
+            assertThat(ex,
+                    publicException(
+                            IgniteException.class,
+                            Common.INTERNAL_ERR,
+                            "Transaction belongs to a different client 
instance",
+                            emptyList()
+                    ).withCause(isA(IllegalArgumentException.class))
+            );
         }
     }
 
@@ -372,8 +384,13 @@ public class ItThinClientTransactionsTest extends 
ItAbstractThinClientTest {
         Transaction tx = client().transactions().begin(new 
TransactionOptions().readOnly(true));
         var ex = assertThrows(TransactionException.class, () -> kvView.put(tx, 
1, "2"));
 
-        assertThat(ex.getMessage(), containsString("Failed to enlist 
read-write operation into read-only transaction"));
-        
assertEquals(ErrorGroups.Transactions.TX_FAILED_READ_WRITE_OPERATION_ERR, 
ex.code());
+        assertThat(ex,
+                publicExceptionWithHint(
+                    TransactionException.class,
+                    
ErrorGroups.Transactions.TX_FAILED_READ_WRITE_OPERATION_ERR,
+                    "Failed to enlist read-write operation into read-only 
transaction"
+                )
+        );
     }
 
     @ParameterizedTest
diff --git 
a/modules/client/src/main/java/org/apache/ignite/internal/client/ClientExceptionMapperProvider.java
 
b/modules/client/src/main/java/org/apache/ignite/internal/client/ClientExceptionMapperProvider.java
new file mode 100644
index 00000000000..d1ddf42012c
--- /dev/null
+++ 
b/modules/client/src/main/java/org/apache/ignite/internal/client/ClientExceptionMapperProvider.java
@@ -0,0 +1,63 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.ignite.internal.client;
+
+import static 
org.apache.ignite.internal.util.ExceptionUtils.copyExceptionWithCause;
+
+import com.google.auto.service.AutoService;
+import java.util.Collection;
+import java.util.List;
+import java.util.function.UnaryOperator;
+import org.apache.ignite.internal.client.tx.ClientTransactionKilledException;
+import org.apache.ignite.internal.lang.IgniteExceptionMapper;
+import org.apache.ignite.internal.lang.IgniteExceptionMappersProvider;
+import org.apache.ignite.internal.lang.IgniteInternalException;
+import org.apache.ignite.lang.IgniteException;
+
+/** Client Module Exception mapper. */
+@AutoService(IgniteExceptionMappersProvider.class)
+public class ClientExceptionMapperProvider implements 
IgniteExceptionMappersProvider {
+    private static final String RETRIABLE_TX_MESSAGE = "Retriable transaction 
exception";
+
+    @Override
+    public Collection<IgniteExceptionMapper<?, ?>> mappers() {
+        return List.of(
+                mapException(
+                        ClientRetriableTransactionException.class,
+                        err -> new 
ClientRetriableTransactionException(err.code(), RETRIABLE_TX_MESSAGE, null)
+                ),
+                mapException(
+                        ClientTransactionKilledException.class,
+                        err -> new 
ClientTransactionKilledException(err.traceId(), err.code(), 
RETRIABLE_TX_MESSAGE, err.txId(), null)
+                )
+        );
+    }
+
+    private static <T extends IgniteInternalException> 
IgniteExceptionMapper<T, IgniteException> mapException(
+            Class<T> errType,
+            UnaryOperator<T> copyFunc
+    ) {
+        return IgniteExceptionMapper.unchecked(errType, err -> {
+            Throwable cause = err.getCause();
+            assert cause.getCause() == null : "Cause of client 
RetriableTransactionExceptions should have no causes.";
+            // Retriable copy is actually a RetriableTransactionException. May 
not be included in the future to present leaking internals.
+            Throwable retriableCopy = copyFunc.apply(err);
+            return copyExceptionWithCause(cause.getClass(), err.traceId(), 
err.code(), err.getMessage(), retriableCopy);
+        });
+    }
+}
diff --git 
a/modules/client/src/main/java/org/apache/ignite/internal/client/ClientRetriableTransactionException.java
 
b/modules/client/src/main/java/org/apache/ignite/internal/client/ClientRetriableTransactionException.java
index dfea7601a62..75d7129c4b6 100644
--- 
a/modules/client/src/main/java/org/apache/ignite/internal/client/ClientRetriableTransactionException.java
+++ 
b/modules/client/src/main/java/org/apache/ignite/internal/client/ClientRetriableTransactionException.java
@@ -17,14 +17,14 @@
 
 package org.apache.ignite.internal.client;
 
-import org.apache.ignite.lang.IgniteException;
+import org.apache.ignite.internal.lang.IgniteInternalException;
 import org.apache.ignite.tx.RetriableTransactionException;
 
 /**
  * Wraps client exception cause for retry purposes, which is based on marker 
interface RetriableTransactionException.
  */
-class ClientRetriableTransactionException extends IgniteException implements 
RetriableTransactionException {
-    public ClientRetriableTransactionException(int code, Throwable cause) {
-        super(code, cause);
+public class ClientRetriableTransactionException extends 
IgniteInternalException implements RetriableTransactionException {
+    public ClientRetriableTransactionException(int code, String msg, Throwable 
cause) {
+        super(code, msg, cause);
     }
 }
diff --git 
a/modules/client/src/main/java/org/apache/ignite/internal/client/TcpClientChannel.java
 
b/modules/client/src/main/java/org/apache/ignite/internal/client/TcpClientChannel.java
index 6ec7fdac4c4..0a9322426a1 100644
--- 
a/modules/client/src/main/java/org/apache/ignite/internal/client/TcpClientChannel.java
+++ 
b/modules/client/src/main/java/org/apache/ignite/internal/client/TcpClientChannel.java
@@ -74,7 +74,6 @@ import org.apache.ignite.internal.logger.IgniteLogger;
 import org.apache.ignite.internal.properties.IgniteProductVersion;
 import org.apache.ignite.internal.thread.PublicApiThreading;
 import org.apache.ignite.internal.tostring.S;
-import org.apache.ignite.internal.util.ViewUtils;
 import org.apache.ignite.lang.ErrorGroups.Table;
 import org.apache.ignite.lang.IgniteException;
 import org.apache.ignite.lang.TraceableException;
@@ -437,7 +436,7 @@ class TcpClientChannel implements ClientChannel, 
ClientMessageHandler, ClientCon
                     return completedFuture(complete(payloadReader, 
notificationFut, unpacker, opCode));
                 } catch (Throwable t) {
                     expectedException = true;
-                    throw sneakyThrow(ViewUtils.ensurePublicException(t));
+                    throw sneakyThrow(t);
                 }
             }
 
@@ -469,7 +468,7 @@ class TcpClientChannel implements ClientChannel, 
ClientMessageHandler, ClientCon
 
             metrics.requestsActiveDecrement();
 
-            throw sneakyThrow(ViewUtils.ensurePublicException(t));
+            throw sneakyThrow(t);
         }
     }
 
@@ -485,11 +484,11 @@ class TcpClientChannel implements ClientChannel, 
ClientMessageHandler, ClientCon
             assert unpacker == null : "unpacker must be null if err is not 
null";
 
             try {
-                asyncContinuationExecutor.execute(() -> 
resFut.completeExceptionally(ViewUtils.ensurePublicException(err)));
+                asyncContinuationExecutor.execute(() -> 
resFut.completeExceptionally(err));
             } catch (Throwable execError) {
                 // Executor error, complete directly.
                 execError.addSuppressed(err);
-                
resFut.completeExceptionally(ViewUtils.ensurePublicException(execError));
+                resFut.completeExceptionally(execError);
             }
 
             return;
@@ -502,14 +501,14 @@ class TcpClientChannel implements ClientChannel, 
ClientMessageHandler, ClientCon
                 try {
                     resFut.complete(complete(payloadReader, notificationFut, 
unpacker, opCode));
                 } catch (Throwable t) {
-                    
resFut.completeExceptionally(ViewUtils.ensurePublicException(t));
+                    resFut.completeExceptionally(t);
                 }
             });
         } catch (Throwable execErr) {
             unpacker.close();
 
             // Executor error, complete directly.
-            
resFut.completeExceptionally(ViewUtils.ensurePublicException(execErr));
+            resFut.completeExceptionally(execErr);
         }
     }
 
@@ -652,12 +651,22 @@ class TcpClientChannel implements ClientChannel, 
ClientMessageHandler, ClientCon
 
         var errClassName = unpacker.unpackString();
         var errMsg = unpacker.tryUnpackNil() ? null : unpacker.unpackString();
-        boolean retriable = false;
+        @Nullable String causeStr = unpacker.tryUnpackNil() ? null : 
unpacker.unpackString();
 
-        IgniteException causeWithStackTrace = unpacker.tryUnpackNil() ? null : 
new IgniteException(traceId, code, unpacker.unpackString());
+        String msg;
+        if (causeStr == null) {
+            msg = errMsg;
+        } else if (errMsg == null) {
+            msg = causeStr;
+        } else {
+            // Remove some duplication between errorMsg and cause.
+            int idx = causeStr.indexOf(errMsg);
+            msg = (idx == -1) ? errMsg + '\n' + causeStr : 
causeStr.substring(idx);
+        }
 
         int extSize = unpacker.tryUnpackNil() ? 0 : unpacker.unpackInt();
         int expectedSchemaVersion = -1;
+        boolean retriable = false;
 
         for (int i = 0; i < extSize; i++) {
             String key = unpacker.unpackString();
@@ -667,14 +676,16 @@ class TcpClientChannel implements ClientChannel, 
ClientMessageHandler, ClientCon
             } else if (key.equals(ErrorExtensions.SQL_UPDATE_COUNTERS)) {
                 // Deprecated format, keep for compat with older servers.
                 return new SqlBatchException(traceId, code, 
unpacker.unpackLongArray(),
-                        errMsg != null ? errMsg : "SQL batch execution error", 
causeWithStackTrace);
+                        msg != null ? msg : "SQL batch execution error", null);
             } else if (key.equals(ErrorExtensions.SQL_UPDATE_COUNTERS_2)) {
                 return new SqlBatchException(traceId, code, 
unpacker.unpackLongArrayAsBinary(),
-                        errMsg != null ? errMsg : "SQL batch execution error", 
causeWithStackTrace);
+                        msg != null ? msg : "SQL batch execution error", null);
             } else if (key.equals(ErrorExtensions.DELAYED_ACK)) {
+                Throwable causeWithStackTrace = createException(errClassName, 
traceId, code, msg, false);
                 return new ClientDelayedAckException(traceId, code, errMsg, 
unpacker.unpackUuid(), causeWithStackTrace);
             } else if (key.equals(ErrorExtensions.TX_KILL)) {
-                return new ClientTransactionKilledException(traceId, code, 
errMsg, unpacker.unpackUuid(), causeWithStackTrace);
+                Throwable causeWithStackTrace = createException(errClassName, 
traceId, code, msg, false);
+                return new ClientTransactionKilledException(traceId, code, 
msg, unpacker.unpackUuid(), causeWithStackTrace);
             } else if (key.equals(ErrorExtensions.FLAGS)) {
                 EnumSet<ErrorFlags> flags = 
ErrorFlags.unpack(unpacker.unpackInt());
                 retriable = flags.contains(ErrorFlags.RETRIABLE);
@@ -685,23 +696,32 @@ class TcpClientChannel implements ClientChannel, 
ClientMessageHandler, ClientCon
         }
 
         if (code == Table.SCHEMA_VERSION_MISMATCH_ERR) {
+            Throwable causeWithStackTrace = createException(errClassName, 
traceId, code, msg, false);
             if (expectedSchemaVersion == -1) {
                 return new IgniteException(
                         traceId, PROTOCOL_ERR, "Expected schema version is not 
specified in error extension map.", causeWithStackTrace);
             }
 
-            return new ClientSchemaVersionMismatchException(traceId, code, 
errMsg, expectedSchemaVersion, causeWithStackTrace);
+            return new ClientSchemaVersionMismatchException(traceId, code, 
msg, expectedSchemaVersion, null);
         }
 
+        return createException(errClassName, traceId, code, msg, retriable);
+    }
+
+    private static Throwable createException(String errClassName, UUID 
traceId, int code, String msg, boolean isRetriable) {
         try {
             Class<? extends Throwable> errCls = (Class<? extends Throwable>) 
Class.forName(errClassName);
-            return copyExceptionWithCause(errCls, traceId, code, errMsg,
-                    retriable ? new ClientRetriableTransactionException(code, 
causeWithStackTrace) : causeWithStackTrace);
+            @Nullable Throwable ex = copyExceptionWithCause(errCls, traceId, 
code, msg, null);
+            if (ex == null) {
+                ex = new IgniteException(traceId, code, msg);
+            }
+
+            return isRetriable
+                    ? new ClientRetriableTransactionException(code, msg, ex)
+                    : ex;
         } catch (ClassNotFoundException ignored) {
-            // Ignore: incompatible exception class. Fall back to generic 
exception.
+            return new IgniteException(traceId, code, errClassName + ": " + 
msg);
         }
-
-        return new IgniteException(traceId, code, errClassName + ": " + 
errMsg, causeWithStackTrace);
     }
 
     /** {@inheritDoc} */
diff --git 
a/modules/client/src/main/java/org/apache/ignite/internal/client/compute/ClientCompute.java
 
b/modules/client/src/main/java/org/apache/ignite/internal/client/compute/ClientCompute.java
index fa258e857cf..4089c213ff2 100644
--- 
a/modules/client/src/main/java/org/apache/ignite/internal/client/compute/ClientCompute.java
+++ 
b/modules/client/src/main/java/org/apache/ignite/internal/client/compute/ClientCompute.java
@@ -20,6 +20,7 @@ package org.apache.ignite.internal.client.compute;
 import static java.util.concurrent.CompletableFuture.allOf;
 import static java.util.concurrent.CompletableFuture.completedFuture;
 import static 
org.apache.ignite.internal.client.TcpIgniteClient.unpackClusterNode;
+import static org.apache.ignite.internal.util.ViewUtils.sync;
 import static org.apache.ignite.lang.ErrorGroups.Client.TABLE_ID_NOT_FOUND_ERR;
 
 import java.util.ArrayList;
@@ -67,7 +68,6 @@ import 
org.apache.ignite.internal.client.table.PartitionAwarenessProvider;
 import org.apache.ignite.internal.compute.BroadcastJobExecutionImpl;
 import org.apache.ignite.internal.compute.FailedExecution;
 import org.apache.ignite.internal.util.ExceptionUtils;
-import org.apache.ignite.internal.util.ViewUtils;
 import org.apache.ignite.lang.CancelHandleHelper;
 import org.apache.ignite.lang.CancellationToken;
 import org.apache.ignite.lang.IgniteException;
@@ -588,12 +588,4 @@ public class ClientCompute implements IgniteCompute {
                 ch.notificationFuture()
         );
     }
-
-    private static <R> R sync(CompletableFuture<R> future) {
-        try {
-            return future.join();
-        } catch (CompletionException e) {
-            throw 
ExceptionUtils.sneakyThrow(ViewUtils.ensurePublicException(e));
-        }
-    }
 }
diff --git 
a/modules/client/src/main/java/org/apache/ignite/internal/client/sql/ClientSql.java
 
b/modules/client/src/main/java/org/apache/ignite/internal/client/sql/ClientSql.java
index 5fb3783f500..694191eb744 100644
--- 
a/modules/client/src/main/java/org/apache/ignite/internal/client/sql/ClientSql.java
+++ 
b/modules/client/src/main/java/org/apache/ignite/internal/client/sql/ClientSql.java
@@ -28,6 +28,8 @@ import static 
org.apache.ignite.internal.client.proto.ProtocolBitmaskFeature.TX_
 import static 
org.apache.ignite.internal.client.proto.ProtocolBitmaskFeature.TX_PIGGYBACK;
 import static org.apache.ignite.internal.util.ExceptionUtils.sneakyThrow;
 import static org.apache.ignite.internal.util.ExceptionUtils.unwrapCause;
+import static org.apache.ignite.internal.util.ViewUtils.ensurePublicException;
+import static org.apache.ignite.internal.util.ViewUtils.sync;
 
 import com.github.benmanes.caffeine.cache.Cache;
 import com.github.benmanes.caffeine.cache.Caffeine;
@@ -38,7 +40,6 @@ import java.util.Map.Entry;
 import java.util.Objects;
 import java.util.Set;
 import java.util.concurrent.CompletableFuture;
-import java.util.concurrent.CompletionException;
 import java.util.concurrent.TimeUnit;
 import java.util.function.BiFunction;
 import org.apache.ignite.internal.binarytuple.BinaryTupleBuilder;
@@ -61,7 +62,6 @@ import 
org.apache.ignite.internal.marshaller.MarshallersProvider;
 import org.apache.ignite.internal.sql.StatementBuilderImpl;
 import org.apache.ignite.internal.sql.StatementImpl;
 import org.apache.ignite.internal.sql.SyncResultSetAdapter;
-import org.apache.ignite.internal.util.ExceptionUtils;
 import org.apache.ignite.lang.CancelHandleHelper;
 import org.apache.ignite.lang.CancellationToken;
 import org.apache.ignite.lang.ErrorGroups.Sql;
@@ -148,12 +148,7 @@ public class ClientSql implements IgniteSql {
             @Nullable Object... arguments
     ) {
         Objects.requireNonNull(query);
-
-        try {
-            return new SyncResultSetAdapter<>(executeAsync(transaction, 
cancellationToken, query, arguments).join());
-        } catch (CompletionException e) {
-            throw sneakyThrow(ExceptionUtils.copyExceptionWithCause(e));
-        }
+        return new SyncResultSetAdapter<>(sync(executeAsync(transaction, 
cancellationToken, query, arguments)));
     }
 
     /** {@inheritDoc} */
@@ -165,12 +160,7 @@ public class ClientSql implements IgniteSql {
             @Nullable Object... arguments
     ) {
         Objects.requireNonNull(statement);
-
-        try {
-            return new SyncResultSetAdapter<>(executeAsync(transaction, 
cancellationToken, statement, arguments).join());
-        } catch (CompletionException e) {
-            throw sneakyThrow(ExceptionUtils.copyExceptionWithCause(e));
-        }
+        return new SyncResultSetAdapter<>(sync(executeAsync(transaction, 
cancellationToken, statement, arguments)));
     }
 
     /** {@inheritDoc} */
@@ -183,12 +173,7 @@ public class ClientSql implements IgniteSql {
             @Nullable Object... arguments
     ) {
         Objects.requireNonNull(query);
-
-        try {
-            return new SyncResultSetAdapter<>(executeAsync(transaction, 
mapper, cancellationToken, query, arguments).join());
-        } catch (CompletionException e) {
-            throw sneakyThrow(ExceptionUtils.copyExceptionWithCause(e));
-        }
+        return new SyncResultSetAdapter<>(sync(executeAsync(transaction, 
mapper, cancellationToken, query, arguments)));
     }
 
     /** {@inheritDoc} */
@@ -201,12 +186,7 @@ public class ClientSql implements IgniteSql {
             @Nullable Object... arguments
     ) {
         Objects.requireNonNull(statement);
-
-        try {
-            return new SyncResultSetAdapter<>(executeAsync(transaction, 
mapper, cancellationToken, statement, arguments).join());
-        } catch (CompletionException e) {
-            throw sneakyThrow(ExceptionUtils.copyExceptionWithCause(e));
-        }
+        return new SyncResultSetAdapter<>(sync(executeAsync(transaction, 
mapper, cancellationToken, statement, arguments)));
     }
 
     /** {@inheritDoc} */
@@ -228,11 +208,7 @@ public class ClientSql implements IgniteSql {
             Statement dmlStatement,
             BatchedArguments batch
     ) {
-        try {
-            return executeBatchAsync(transaction, cancellationToken, 
dmlStatement, batch).join();
-        } catch (CompletionException e) {
-            throw sneakyThrow(ExceptionUtils.copyExceptionWithCause(e));
-        }
+        return sync(executeBatchAsync(transaction, cancellationToken, 
dmlStatement, batch));
     }
 
     /** {@inheritDoc} */
@@ -245,12 +221,7 @@ public class ClientSql implements IgniteSql {
     @Override
     public void executeScript(@Nullable CancellationToken cancellationToken, 
String query, @Nullable Object... arguments) {
         Objects.requireNonNull(query);
-
-        try {
-            executeScriptAsync(cancellationToken, query, arguments).join();
-        } catch (CompletionException e) {
-            throw sneakyThrow(ExceptionUtils.copyExceptionWithCause(e));
-        }
+        sync(executeScriptAsync(cancellationToken, query, arguments));
     }
 
     /** {@inheritDoc} */
@@ -693,7 +664,8 @@ public class ClientSql implements IgniteSql {
     }
 
     private static <T> T handleException(Throwable e) {
-        Throwable ex = unwrapCause(e);
+        Throwable ex = ensurePublicException(unwrapCause(e));
+
         if (ex instanceof TransactionException) {
             var te = (TransactionException) ex;
             throw new SqlException(te.traceId(), te.code(), te.getMessage(), 
te);
diff --git 
a/modules/client/src/main/java/org/apache/ignite/internal/client/table/ClientTable.java
 
b/modules/client/src/main/java/org/apache/ignite/internal/client/table/ClientTable.java
index 3b90a75e62a..b4b944c13d5 100644
--- 
a/modules/client/src/main/java/org/apache/ignite/internal/client/table/ClientTable.java
+++ 
b/modules/client/src/main/java/org/apache/ignite/internal/client/table/ClientTable.java
@@ -66,6 +66,7 @@ import 
org.apache.ignite.internal.marshaller.MarshallersProvider;
 import org.apache.ignite.internal.marshaller.UnmappedColumnsException;
 import org.apache.ignite.internal.tostring.IgniteToStringBuilder;
 import org.apache.ignite.internal.util.IgniteUtils;
+import org.apache.ignite.internal.util.ViewUtils;
 import org.apache.ignite.lang.IgniteException;
 import org.apache.ignite.table.KeyValueView;
 import org.apache.ignite.table.QualifiedName;
@@ -585,7 +586,14 @@ public class ClientTable implements Table {
                     return null;
                 });
 
-        return fut;
+        return fut.handle((v, err) -> {
+            if (err == null) {
+                return v;
+            }
+
+            var cause = unwrapCause(err);
+            throw sneakyThrow(ViewUtils.ensurePublicException(cause));
+        });
     }
 
     private <T> @Nullable Object readSchemaAndReadData(
diff --git 
a/modules/client/src/main/java/org/apache/ignite/internal/client/tx/ClientTransactionKilledException.java
 
b/modules/client/src/main/java/org/apache/ignite/internal/client/tx/ClientTransactionKilledException.java
index a79bc88d2d8..f22b9e48edf 100644
--- 
a/modules/client/src/main/java/org/apache/ignite/internal/client/tx/ClientTransactionKilledException.java
+++ 
b/modules/client/src/main/java/org/apache/ignite/internal/client/tx/ClientTransactionKilledException.java
@@ -18,14 +18,14 @@
 package org.apache.ignite.internal.client.tx;
 
 import java.util.UUID;
+import org.apache.ignite.internal.lang.IgniteInternalException;
 import org.apache.ignite.tx.RetriableTransactionException;
-import org.apache.ignite.tx.TransactionException;
 import org.jetbrains.annotations.Nullable;
 
 /**
  * Reports a killed transaction.
  */
-public class ClientTransactionKilledException extends TransactionException 
implements RetriableTransactionException {
+public class ClientTransactionKilledException extends IgniteInternalException 
implements RetriableTransactionException {
     /** Serial version uid. */
     private static final long serialVersionUID = 0L;
 
diff --git 
a/modules/client/src/test/java/org/apache/ignite/client/ConnectionTest.java 
b/modules/client/src/test/java/org/apache/ignite/client/ConnectionTest.java
index a3b3c15faad..d3ab2406a62 100644
--- a/modules/client/src/test/java/org/apache/ignite/client/ConnectionTest.java
+++ b/modules/client/src/test/java/org/apache/ignite/client/ConnectionTest.java
@@ -77,7 +77,7 @@ public class ConnectionTest extends AbstractClientTest {
         var ex = assertThrows(IgniteClientConnectionException.class,
                 () -> testConnection("127.0.0.1:47500"));
 
-        String errMsg = ex.getCause().getMessage();
+        String errMsg = ex.getMessage();
 
         // It does not seem possible to verify that it's a 'Connection 
refused' exception because with different
         // user locales the message differs, so let's just check that the 
message ends with the known suffix.
diff --git 
a/modules/compute/src/integrationTest/java/org/apache/ignite/internal/compute/ItComputeBaseTest.java
 
b/modules/compute/src/integrationTest/java/org/apache/ignite/internal/compute/ItComputeBaseTest.java
index 02043f31c41..49e9691e90c 100644
--- 
a/modules/compute/src/integrationTest/java/org/apache/ignite/internal/compute/ItComputeBaseTest.java
+++ 
b/modules/compute/src/integrationTest/java/org/apache/ignite/internal/compute/ItComputeBaseTest.java
@@ -25,6 +25,7 @@ import static org.apache.ignite.compute.JobStatus.EXECUTING;
 import static org.apache.ignite.compute.JobStatus.FAILED;
 import static org.apache.ignite.compute.JobStatus.QUEUED;
 import static org.apache.ignite.internal.IgniteExceptionTestUtils.hasMessage;
+import static 
org.apache.ignite.internal.IgniteExceptionTestUtils.publicException;
 import static 
org.apache.ignite.internal.IgniteExceptionTestUtils.traceableException;
 import static org.apache.ignite.internal.TestWrappers.unwrapIgniteImpl;
 import static org.apache.ignite.internal.lang.IgniteStringFormatter.format;
@@ -44,7 +45,6 @@ import static org.hamcrest.Matchers.both;
 import static org.hamcrest.Matchers.contains;
 import static org.hamcrest.Matchers.containsInAnyOrder;
 import static org.hamcrest.Matchers.containsString;
-import static org.hamcrest.Matchers.either;
 import static org.hamcrest.Matchers.everyItem;
 import static org.hamcrest.Matchers.hasSize;
 import static org.hamcrest.Matchers.in;
@@ -84,6 +84,7 @@ import org.apache.ignite.compute.task.TaskExecution;
 import org.apache.ignite.deployment.DeploymentUnit;
 import org.apache.ignite.internal.ClusterPerClassIntegrationTest;
 import org.apache.ignite.internal.ConfigOverride;
+import org.apache.ignite.internal.IgniteExceptionTestUtils.Cause;
 import org.apache.ignite.internal.hlc.HybridTimestamp;
 import org.apache.ignite.internal.testframework.IgniteTestUtils;
 import org.apache.ignite.lang.CancelHandle;
@@ -115,6 +116,8 @@ import org.junit.jupiter.params.provider.ValueSource;
 public abstract class ItComputeBaseTest extends ClusterPerClassIntegrationTest 
{
     protected abstract List<DeploymentUnit> units();
 
+    protected abstract ClientType clientType();
+
     protected String jobPackage() {
         return "org.example.jobs.embedded";
     }
@@ -310,7 +313,7 @@ public abstract class ItComputeBaseTest extends 
ClusterPerClassIntegrationTest {
                 JobTarget.node(clusterNode(entryNode)),
                 failingJob(), null));
 
-        assertThat(ex, is(computeJobFailedException("JobException", "Oops")));
+        assertThat(ex, 
is(computeJobFailedException("org.example.jobs.embedded.JobException", 
"Oops")));
     }
 
     @Test
@@ -323,7 +326,7 @@ public abstract class ItComputeBaseTest extends 
ClusterPerClassIntegrationTest {
                 null
         );
 
-        assertThat(execution.resultAsync(), 
willThrow(computeJobFailedException("JobException", "Oops")));
+        assertThat(execution.resultAsync(), 
willThrow(computeJobFailedException("org.example.jobs.embedded.JobException", 
"Oops")));
 
         assertThat(execution.stateAsync(), willBe(jobStateWithStatus(FAILED)));
     }
@@ -334,7 +337,7 @@ public abstract class ItComputeBaseTest extends 
ClusterPerClassIntegrationTest {
                 JobTarget.anyNode(clusterNode(node(1)), clusterNode(node(2))),
                 failingJob(), null));
 
-        assertThat(ex, is(computeJobFailedException("JobException", "Oops")));
+        assertThat(ex, 
is(computeJobFailedException("org.example.jobs.embedded.JobException", 
"Oops")));
     }
 
     @Test
@@ -358,7 +361,7 @@ public abstract class ItComputeBaseTest extends 
ClusterPerClassIntegrationTest {
                 null
         );
 
-        assertThat(execution.resultAsync(), 
willThrow(computeJobFailedException("JobException", "Oops")));
+        assertThat(execution.resultAsync(), 
willThrow(computeJobFailedException("org.example.jobs.embedded.JobException", 
"Oops")));
 
         assertThat(execution.stateAsync(), willBe(jobStateWithStatus(FAILED)));
     }
@@ -412,12 +415,13 @@ public abstract class ItComputeBaseTest extends 
ClusterPerClassIntegrationTest {
         Collection<JobExecution<String>> executions = 
broadcastExecution.executions();
         assertThat(executions, hasSize(3));
         for (JobExecution<String> execution : executions) {
-            assertThat(execution.resultAsync(), 
willThrow(computeJobFailedException("JobException", "Oops")));
+            assertThat(execution.resultAsync(), 
willThrow(computeJobFailedException("org.example.jobs.embedded.JobException", 
"Oops")));
 
             assertThat(execution.stateAsync(), 
willBe(jobStateWithStatus(FAILED)));
         }
 
-        assertThat(broadcastExecution.resultsAsync(), 
willThrow(computeJobFailedException("JobException", "Oops")));
+        assertThat(broadcastExecution.resultsAsync(),
+                
willThrow(computeJobFailedException("org.example.jobs.embedded.JobException", 
"Oops")));
     }
 
     @Test
@@ -669,17 +673,8 @@ public abstract class ItComputeBaseTest extends 
ClusterPerClassIntegrationTest {
         assertThat(cancelHandle.cancelAsync(), willCompleteSuccessfully());
 
         CompletionException completionException = 
assertThrows(CompletionException.class, () -> execution.resultAsync().join());
-
-        // Unwrap CompletionException, ComputeException should be the cause 
thrown from the API
-        assertThat(completionException.getCause(), 
instanceOf(ComputeException.class));
-        ComputeException computeException = (ComputeException) 
completionException.getCause();
-
-        // ComputeException should be caused by the RuntimeException thrown 
from the SleepJob
-        assertThat(computeException.getCause(), 
instanceOf(RuntimeException.class));
-        RuntimeException runtimeException = (RuntimeException) 
computeException.getCause();
-
-        // RuntimeException is thrown when SleepJob catches the 
InterruptedException
-        assertThat(runtimeException.toString(), 
containsString(InterruptedException.class.getName()));
+        assertThat((Exception) completionException.getCause(),
+                
computeJobFailedException(InterruptedException.class.getName(), "sleep 
interrupted"));
 
         await().until(execution::stateAsync, 
willBe(jobStateWithStatus(FAILED)));
     }
@@ -1061,29 +1056,63 @@ public abstract class ItComputeBaseTest extends 
ClusterPerClassIntegrationTest {
                 .units(units()).build();
     }
 
-    static Matcher<Exception> computeJobFailedException(String causeClass, 
String causeMsgSubstring) {
-        return traceableException(ComputeException.class)
-                .withCode(is(COMPUTE_JOB_FAILED_ERR))
-                .withMessage(both(containsString("Job execution failed:"))
-                        .and(containsString(causeClass)))
-                .withCause(hasMessage(containsString(causeMsgSubstring)));
+    Matcher<Exception> computeJobFailedException(String causeClass, String 
causeMsgSubstring) {
+        return computeJobFailedException(clientType(), causeClass, 
causeMsgSubstring);
+    }
+
+    static Matcher<Exception> computeJobFailedException(ClientType clientType, 
String causeClass, String causeMsgSubstring) {
+        var msgMatcher = both(containsString("Job execution 
failed:")).and(containsString(causeClass));
+        switch (clientType) {
+            case JAVA:
+                return publicException(
+                        ComputeException.class,
+                        COMPUTE_JOB_FAILED_ERR,
+                        "",
+                        List.of(new Cause(causeClass, causeMsgSubstring))
+                )
+                        .withMessage(msgMatcher);
+            case EMBEDDED:
+                return traceableException(ComputeException.class)
+                        .withCode(is(COMPUTE_JOB_FAILED_ERR))
+                        .withMessage(msgMatcher)
+                        
.withCause(hasMessage(containsString(causeMsgSubstring)));
+            default:
+                throw new IllegalArgumentException("invalid clientType");
+        }
+    }
+
+    Matcher<Exception> computeJobCancelledException() {
+        return computeJobCancelledException(clientType());
     }
 
-    private static Matcher<Exception> computeJobCancelledException() {
-        return traceableException(ComputeException.class)
-                .withCode(is(COMPUTE_JOB_CANCELLED_ERR))
-                .withMessage(containsString("Job execution cancelled"))
-                .withCause(
-                        // Thin client exception transfers the class name in a 
message of the cause,
-                        // embedded exception are instances in the cause chain
-                        
either(hasMessage(containsString(CancellationException.class.getName())))
-                                .or(instanceOf(CancellationException.class))
+    private static Matcher<Exception> computeJobCancelledException(ClientType 
clientType) {
+        switch (clientType) {
+            case JAVA:
+                return publicException(
+                        ComputeException.class,
+                        COMPUTE_JOB_CANCELLED_ERR,
+                        "Job execution cancelled",
+                        List.of(Cause.of(CancellationException.class))
                 );
+            case EMBEDDED:
+                return traceableException(ComputeException.class)
+                        .withCode(is(COMPUTE_JOB_CANCELLED_ERR))
+                        .withMessage(containsString("Job execution cancelled"))
+                        .withCause(instanceOf(CancellationException.class));
+            default:
+                throw new IllegalArgumentException("invalid clientType");
+        }
+    }
+
+    /** ClientType. */
+    public enum ClientType {
+        JAVA,
+        EMBEDDED,
     }
 
     private static Matcher<Exception> sqlCancelledException() {
         return traceableException(SqlException.class)
                 .withCode(is(EXECUTION_CANCELLED_ERR))
-                .withMessage(is("The query was cancelled while executing."));
+                .withMessage(containsString("The query was cancelled while 
executing."));
     }
 }
diff --git 
a/modules/compute/src/integrationTest/java/org/apache/ignite/internal/compute/ItComputeStandaloneTest.java
 
b/modules/compute/src/integrationTest/java/org/apache/ignite/internal/compute/ItComputeStandaloneTest.java
index ea87573194c..2780da3be3a 100644
--- 
a/modules/compute/src/integrationTest/java/org/apache/ignite/internal/compute/ItComputeStandaloneTest.java
+++ 
b/modules/compute/src/integrationTest/java/org/apache/ignite/internal/compute/ItComputeStandaloneTest.java
@@ -111,6 +111,11 @@ class ItComputeStandaloneTest extends ItComputeBaseTest {
         return units;
     }
 
+    @Override
+    protected ClientType clientType() {
+        return ClientType.EMBEDDED;
+    }
+
     @Disabled("https://issues.apache.org/jira/browse/IGNITE-26546";)
     @Override
     void executesFailingJobLocally() {
diff --git 
a/modules/compute/src/integrationTest/java/org/apache/ignite/internal/compute/ItComputeTestClient.java
 
b/modules/compute/src/integrationTest/java/org/apache/ignite/internal/compute/ItComputeTestClient.java
index 1502718c99f..69c78bda59a 100644
--- 
a/modules/compute/src/integrationTest/java/org/apache/ignite/internal/compute/ItComputeTestClient.java
+++ 
b/modules/compute/src/integrationTest/java/org/apache/ignite/internal/compute/ItComputeTestClient.java
@@ -31,6 +31,11 @@ import org.junit.jupiter.api.AfterEach;
 public class ItComputeTestClient extends ItComputeTestEmbedded {
     private final Clients clients = new Clients();
 
+    @Override
+    protected ClientType clientType() {
+        return ClientType.JAVA;
+    }
+
     @AfterEach
     void stopClient() {
         clients.cleanup();
diff --git 
a/modules/compute/src/integrationTest/java/org/apache/ignite/internal/compute/ItComputeTestEmbedded.java
 
b/modules/compute/src/integrationTest/java/org/apache/ignite/internal/compute/ItComputeTestEmbedded.java
index 1f856afd549..151a0114529 100644
--- 
a/modules/compute/src/integrationTest/java/org/apache/ignite/internal/compute/ItComputeTestEmbedded.java
+++ 
b/modules/compute/src/integrationTest/java/org/apache/ignite/internal/compute/ItComputeTestEmbedded.java
@@ -76,6 +76,11 @@ class ItComputeTestEmbedded extends ItComputeBaseTest {
         return List.of();
     }
 
+    @Override
+    protected ClientType clientType() {
+        return ClientType.EMBEDDED;
+    }
+
     @SuppressWarnings("AssignmentToStaticFieldFromInstanceMethod")
     @Test
     void changeJobPriorityLocally() {
@@ -232,7 +237,7 @@ class ItComputeTestEmbedded extends ItComputeBaseTest {
                 
JobDescriptor.builder(CustomFailingJob.class).units(units()).build(),
                 null));
 
-        assertThat(ex, 
is(computeJobFailedException(throwable.getClass().getName(), 
throwable.getMessage())));
+        assertThat(ex, is(computeJobFailedException(ClientType.EMBEDDED, 
throwable.getClass().getName(), throwable.getMessage())));
     }
 
     @ParameterizedTest
diff --git 
a/modules/core/src/main/java/org/apache/ignite/internal/util/ViewUtils.java 
b/modules/core/src/main/java/org/apache/ignite/internal/util/ViewUtils.java
index 16623becb5b..3b029ffc8ed 100644
--- a/modules/core/src/main/java/org/apache/ignite/internal/util/ViewUtils.java
+++ b/modules/core/src/main/java/org/apache/ignite/internal/util/ViewUtils.java
@@ -21,10 +21,18 @@ import static 
org.apache.ignite.internal.util.ExceptionUtils.sneakyThrow;
 import static org.apache.ignite.internal.util.ExceptionUtils.unwrapCause;
 import static org.apache.ignite.lang.ErrorGroups.Common.INTERNAL_ERR;
 
+import java.lang.invoke.MethodHandle;
+import java.lang.invoke.MethodHandles;
+import java.lang.invoke.MethodType;
 import java.util.Collection;
+import java.util.Map;
 import java.util.Objects;
+import java.util.Optional;
+import java.util.concurrent.CancellationException;
 import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.ConcurrentHashMap;
 import java.util.concurrent.ExecutionException;
+import java.util.function.Function;
 import org.apache.ignite.internal.lang.IgniteExceptionMapperUtil;
 import org.apache.ignite.lang.IgniteCheckedException;
 import org.apache.ignite.lang.IgniteException;
@@ -34,6 +42,8 @@ import org.apache.ignite.lang.TraceableException;
  * Table view utilities.
  */
 public final class ViewUtils {
+    private static final Map<Class<?>, Optional<MethodHandle>> 
COPY_FACTORY_METHOD_CACHE = new ConcurrentHashMap<>();
+
     /**
      * Waits for async operation completion.
      *
@@ -48,7 +58,7 @@ public final class ViewUtils {
             Thread.currentThread().interrupt(); // Restore interrupt flag.
 
             throw sneakyThrow(ensurePublicException(e));
-        } catch (ExecutionException e) {
+        } catch (ExecutionException | CancellationException e) {
             Throwable cause = unwrapCause(e);
 
             throw sneakyThrow(ensurePublicException(cause));
@@ -56,8 +66,13 @@ public final class ViewUtils {
     }
 
     /**
-     * Wraps an exception in an IgniteException, extracting trace identifier 
and error code when the specified exception or one of its
-     * causes is an IgniteException itself.
+     * Ensures the provided exception complies with our public API.
+     * <ol>
+     *   <li>Errors caused by {@link IgniteException} and {@link 
IgniteCheckedException} are treated as server-side exceptions.
+     *      Their stack trace is rebased to the current stack trace.</li>
+     *   <li>Other errors are mapped using {@link 
IgniteExceptionMapperUtil#mapToPublicException(Throwable, Function)},
+     *      are treated as client-side errors, and their stack trace is not 
rebased.</li>
+     * </ol>
      *
      * @param e Internal exception.
      * @return Public exception.
@@ -65,19 +80,12 @@ public final class ViewUtils {
     public static Throwable ensurePublicException(Throwable e) {
         Objects.requireNonNull(e);
 
-        e = unwrapCause(e);
-
-        if (e instanceof IgniteException) {
-            return copyExceptionWithCauseIfPossible((IgniteException) e);
-        }
-
-        if (e instanceof IgniteCheckedException) {
-            return copyExceptionWithCauseIfPossible((IgniteCheckedException) 
e);
+        // Copy should rebase the stacktrace.
+        if (e instanceof IgniteException || e instanceof 
IgniteCheckedException) {
+            return copyExceptionWithCauseIfPossible((Throwable & 
TraceableException) e);
         }
 
-        var e0 = IgniteExceptionMapperUtil.mapToPublicException(e);
-
-        return new IgniteException(INTERNAL_ERR, e0.getMessage(), e0);
+        return IgniteExceptionMapperUtil.mapToPublicException(e, ex -> new 
IgniteException(INTERNAL_ERR, ex.getMessage(), ex));
     }
 
     /**
@@ -88,7 +96,20 @@ public final class ViewUtils {
      */
     // TODO: consider removing after IGNITE-22721 gets resolved.
     private static <T extends Throwable & TraceableException> Throwable 
copyExceptionWithCauseIfPossible(T e) {
-        Throwable copy = ExceptionUtils.copyExceptionWithCause(e.getClass(), 
e.traceId(), e.code(), e.getMessage(), e);
+        // Copy exception with cause does not respect custom exception fields.
+        // TODO: IGNITE-28422 We should just create this during compile time 
and call it a day
+        Optional<MethodHandle> copyMethodHandleOpt = 
COPY_FACTORY_METHOD_CACHE.computeIfAbsent(e.getClass(),
+                ViewUtils::createMethodHandleForCopy);
+
+        if (copyMethodHandleOpt.isPresent()) {
+            try {
+                return (T) copyMethodHandleOpt.get().invoke(e);
+            } catch (Throwable ignored) {
+                // Intentionally left blank.
+            }
+        }
+
+        Throwable copy = ExceptionUtils.copyExceptionWithCause(e.getClass(), 
e.traceId(), e.code(), e.getMessage(), e.getCause());
         if (copy != null) {
             return copy;
         }
@@ -97,6 +118,21 @@ public final class ViewUtils {
                 + e.getClass().getName(), e);
     }
 
+    private static Optional<MethodHandle> createMethodHandleForCopy(Class<?> 
type) {
+        try {
+            MethodHandles.Lookup privateLookup = 
MethodHandles.privateLookupIn(type, MethodHandles.lookup());
+            MethodHandle mhandle = privateLookup.findStatic(
+                    type,
+                    "copy",
+                    MethodType.methodType(type, type) // adjust signature
+            );
+
+            return Optional.of(mhandle);
+        } catch (IllegalAccessException | NoSuchMethodException ignored) {
+            return Optional.empty();
+        }
+    }
+
     /**
      * Checks that given keys collection isn't null and there is no a 
null-value key.
      *
diff --git 
a/modules/core/src/test/java/org/apache/ignite/internal/util/ViewUtilsTest.java 
b/modules/core/src/test/java/org/apache/ignite/internal/util/ViewUtilsTest.java
index 9a5d89ae976..1bbc21e9a6c 100644
--- 
a/modules/core/src/test/java/org/apache/ignite/internal/util/ViewUtilsTest.java
+++ 
b/modules/core/src/test/java/org/apache/ignite/internal/util/ViewUtilsTest.java
@@ -23,6 +23,7 @@ import static org.hamcrest.Matchers.containsString;
 import static org.hamcrest.Matchers.hasToString;
 import static org.junit.jupiter.api.Assertions.assertEquals;
 import static org.junit.jupiter.api.Assertions.assertInstanceOf;
+import static org.junit.jupiter.api.Assertions.assertNull;
 import static org.junit.jupiter.api.Assertions.assertSame;
 import static org.junit.jupiter.api.Assertions.assertThrows;
 
@@ -47,7 +48,7 @@ public class ViewUtilsTest {
         assertEquals(ex.getMessage(), resEx.getMessage());
         assertThat(Arrays.asList(ex.getStackTrace()), 
hasToString(containsString("throwIgniteException")));
         assertThat(Arrays.asList(resEx.getStackTrace()), 
hasToString(containsString("checkableTestMethod")));
-        assertSame(ex.getClass(), resEx.getCause().getClass());
+        assertNull(resEx.getCause());
     }
 
     @Test
@@ -61,7 +62,7 @@ public class ViewUtilsTest {
         assertEquals(ex.getMessage(), resEx.getMessage());
         assertThat(Arrays.asList(ex.getStackTrace()), 
hasToString(containsString("throwIgniteCheckedException")));
         assertThat(Arrays.asList(resEx.getStackTrace()), 
hasToString(containsString("checkableTestMethod")));
-        assertSame(ex.getClass(), resEx.getCause().getClass());
+        assertNull(resEx.getCause());
     }
 
     @Test
@@ -75,8 +76,7 @@ public class ViewUtilsTest {
         assertEquals(ex.getMessage(), resEx.getMessage());
         assertThat(Arrays.asList(ex.getStackTrace()), 
hasToString(containsString("throwRuntimeException")));
         assertThat(Arrays.asList(resEx.getStackTrace()), 
hasToString(containsString("checkableTestMethod")));
-        assertSame(IgniteException.class, resEx.getCause().getClass());
-        assertSame(ex.getClass(), resEx.getCause().getCause().getClass());
+        assertSame(ex, resEx.getCause());
     }
 
     @Test
@@ -89,8 +89,7 @@ public class ViewUtilsTest {
         assertThat(resEx.getMessage(), containsString("Public Ignite 
exception-derived class does not have required constructor"));
         assertThat(Arrays.asList(ex.getStackTrace()), 
hasToString(containsString("throwInvalidIgniteException")));
         assertThat(Arrays.asList(resEx.getStackTrace()), 
hasToString(containsString("checkableTestMethod")));
-        assertSame(InvalidIgniteException.class, resEx.getCause().getClass());
-        assertSame(ex.getClass(), resEx.getCause().getClass());
+        assertSame(ex, resEx.getCause());
     }
 
     /**
diff --git 
a/modules/core/src/testFixtures/java/org/apache/ignite/internal/IgniteExceptionTestUtils.java
 
b/modules/core/src/testFixtures/java/org/apache/ignite/internal/IgniteExceptionTestUtils.java
index 9d3b6d09d93..dd20af60887 100644
--- 
a/modules/core/src/testFixtures/java/org/apache/ignite/internal/IgniteExceptionTestUtils.java
+++ 
b/modules/core/src/testFixtures/java/org/apache/ignite/internal/IgniteExceptionTestUtils.java
@@ -17,14 +17,31 @@
 
 package org.apache.ignite.internal;
 
+import static org.apache.ignite.internal.util.ExceptionUtils.unwrapCause;
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.allOf;
+import static org.hamcrest.Matchers.anyOf;
 import static org.hamcrest.Matchers.containsString;
+import static org.hamcrest.Matchers.instanceOf;
 import static org.hamcrest.Matchers.is;
+import static org.hamcrest.Matchers.nullValue;
 
+import java.lang.reflect.InvocationHandler;
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+import java.lang.reflect.Proxy;
+import java.util.List;
+import java.util.concurrent.CompletableFuture;
+import org.apache.ignite.internal.util.ExceptionUtils;
 import org.apache.ignite.lang.IgniteCheckedException;
 import org.apache.ignite.lang.IgniteException;
 import org.apache.ignite.lang.TraceableException;
+import org.apache.ignite.sql.IgniteSql;
+import org.apache.ignite.table.KeyValueView;
+import org.apache.ignite.tx.RetriableTransactionException;
 import org.hamcrest.FeatureMatcher;
 import org.hamcrest.Matcher;
+import org.jetbrains.annotations.Nullable;
 
 /**
  * Test utils for checking public exceptions.
@@ -67,6 +84,69 @@ public class IgniteExceptionTestUtils {
         return traceableException(IgniteException.class, expectedCode, 
containMessage);
     }
 
+    /**
+     * Creates a matcher for public exceptions with stacktrace sent from the 
server.
+     *
+     * @param expectedClass expected exception type.
+     * @param expectedCode expected code.
+     * @param containMessage message that exception should contain.
+     * @param causes Expected causes to be in the server sent stacktrace.
+     * @return message that exception should contain.
+     */
+    public static TraceableExceptionMatcher publicException(
+            Class<? extends TraceableException> expectedClass,
+            int expectedCode,
+            String containMessage,
+            List<Cause> causes
+    ) {
+        var ret = traceableException(expectedClass)
+                .withCode(is(expectedCode))
+                .withMessage(containsString(containMessage))
+                .withCause(
+                        // Checks if is either null or a RetriableException 
with no cause and same code and trace.
+                        anyOf(
+                                nullValue(Throwable.class),
+                                allOf(
+                                        
instanceOf(RetriableTransactionException.class),
+                                        
traceableException(TraceableException.class)
+                                                .withCode(is(expectedCode))
+                                                
.withCause(nullValue(Throwable.class))
+                                )
+                        )
+                );
+
+        for (var cause : causes) {
+            if (cause.message() != null) {
+                ret = ret.withMessage(containsString(String.format("Caused by: 
%s: %s", cause.className(), cause.message())));
+            } else {
+                ret = ret.withMessage(containsString(String.format("Caused by: 
%s", cause.className())));
+            }
+        }
+
+        return ret;
+    }
+
+    /**
+     * Creates an exception matcher with stacktrace not sent from the server.
+     *
+     * @param expectedClass expected exception type.
+     * @param expectedCode expected code.
+     * @param containMessage message that exception should contain.
+     * @return message that exception should contain.
+     */
+    public static TraceableExceptionMatcher publicExceptionWithHint(
+            Class<? extends TraceableException> expectedClass,
+            int expectedCode,
+            String containMessage
+    ) {
+        return traceableException(expectedClass)
+                .withCode(is(expectedCode))
+                .withMessage(containsString(containMessage))
+                .withMessage(containsString("To see the full stack trace, "
+                        + "set 
clientConnector.sendServerExceptionStackTraceToClient:true on the server"))
+                .withCause(nullValue(Throwable.class));
+    }
+
     /**
      * Creates a matcher that matches a public checked exception with expected 
code and message.
      *
@@ -90,4 +170,127 @@ public class IgniteExceptionTestUtils {
             }
         };
     }
+
+    /**
+     * Creates a matcher that checks if a given exception complies with our 
public guidelines.
+     *
+     * @return Matcher.
+     */
+    public static Matcher<Exception> anyPublicException() {
+        return allOf(
+                anyOf(instanceOf(IgniteException.class), 
instanceOf(IgniteCheckedException.class)),
+                traceableException(TraceableException.class)
+                        .withCause(
+                                // Checks if is either null or a 
RetriableException with no cause and same code and trace.
+                                anyOf(
+                                        nullValue(Throwable.class),
+                                        allOf(
+                                                
instanceOf(RetriableTransactionException.class),
+                                                
traceableException(TraceableException.class)
+                                                        
.withCause(nullValue(Throwable.class))
+                                        )
+                                )
+                        )
+        );
+    }
+
+    /**
+     * Wraps a KeyValueView with a proxy that checks that all exceptions 
thrown by it comply with the public guidelines.
+     *
+     * @param view View.
+     * @param <K> KeyType.
+     * @param <V> ValueType.
+     * @return Proxy around the view.
+     */
+    public static <K, V> KeyValueView<K, V> 
withPublicExceptionAssertions(KeyValueView<K, V> view) {
+        return (KeyValueView<K, V>) Proxy.newProxyInstance(
+                KeyValueView.class.getClassLoader(),
+                new Class<?>[]{KeyValueView.class},
+                new PublicExceptionCheckInvocationHandler<>(view)
+        );
+    }
+
+    /**
+     * Wraps a IgniteSql with a proxy that checks that all exceptions thrown 
by it comply with the public guidelines.
+     *
+     * @param view IgniteSql instance..
+     * @return Proxy around the IgniteSql.
+     */
+    public static IgniteSql withPublicExceptionAssertions(IgniteSql view) {
+        return (IgniteSql) Proxy.newProxyInstance(
+                IgniteSql.class.getClassLoader(),
+                new Class<?>[]{IgniteSql.class},
+                new PublicExceptionCheckInvocationHandler<>(view)
+        );
+    }
+
+    private static class PublicExceptionCheckInvocationHandler<T> implements 
InvocationHandler {
+        private final T target;
+
+        PublicExceptionCheckInvocationHandler(T target) {
+            this.target = target;
+        }
+
+        @Override
+        public Object invoke(Object proxy, Method method, Object[] args) 
throws Throwable {
+            boolean isAsync = 
CompletableFuture.class.isAssignableFrom(method.getReturnType());
+
+            if (isAsync) {
+                try {
+                    CompletableFuture<?> future = (CompletableFuture<?>) 
method.invoke(target, args);
+                    return future.handle((r, e) -> {
+                        if (e != null) {
+                            Exception ex = (Exception) unwrapCause(e);
+                            assertThat(ex, anyPublicException());
+                            ExceptionUtils.sneakyThrow(e);
+                        }
+
+                        return r;
+                    });
+                } catch (InvocationTargetException e) {
+                    return CompletableFuture.failedFuture(e);
+                }
+            } else {
+                try {
+                    return method.invoke(target, args);
+                } catch (InvocationTargetException e) {
+                    throw e;
+                } catch (Exception e) {
+                    assertThat(e, anyPublicException());
+                    throw e;
+                }
+            }
+        }
+    }
+
+    /** Cause. */
+    public static class Cause {
+        private final String className;
+
+        // May be null, indicates no matcher will be used.
+        @Nullable
+        private final String message;
+
+        public Cause(String className, @Nullable String message) {
+            this.className = className;
+            this.message = message;
+        }
+
+        public String className() {
+            return className;
+        }
+
+        @Nullable
+        public String message() {
+            return message;
+        }
+
+        public static Cause of(Class<?> klass) {
+            return new Cause(klass.getName(), null);
+        }
+
+        public static Cause of(Class<?> klass, String message) {
+            return new Cause(klass.getName(), message);
+        }
+    }
 }
diff --git 
a/modules/core/src/testFixtures/java/org/apache/ignite/internal/TraceableExceptionMatcher.java
 
b/modules/core/src/testFixtures/java/org/apache/ignite/internal/TraceableExceptionMatcher.java
index 602db608294..4d600c1ca04 100644
--- 
a/modules/core/src/testFixtures/java/org/apache/ignite/internal/TraceableExceptionMatcher.java
+++ 
b/modules/core/src/testFixtures/java/org/apache/ignite/internal/TraceableExceptionMatcher.java
@@ -39,7 +39,7 @@ public class TraceableExceptionMatcher extends 
TypeSafeMatcher<Exception> {
 
     private Matcher<Integer> codeMatcher;
 
-    private final Matcher<UUID> traceIdMatcher = is(notNullValue(UUID.class));
+    private Matcher<UUID> traceIdMatcher = is(notNullValue(UUID.class));
 
     private Matcher<String> messageMatcher;
 
@@ -83,6 +83,11 @@ public class TraceableExceptionMatcher extends 
TypeSafeMatcher<Exception> {
         return this;
     }
 
+    public TraceableExceptionMatcher withTraceId(Matcher<UUID> traceIdMatcher) 
{
+        this.traceIdMatcher = traceIdMatcher;
+        return this;
+    }
+
     @Override
     protected boolean matchesSafely(Exception item) {
         Throwable throwable = ExceptionUtils.unwrapCause(item);
@@ -99,6 +104,10 @@ public class TraceableExceptionMatcher extends 
TypeSafeMatcher<Exception> {
     }
 
     private boolean matchesWithCause(Throwable e) {
+        if (e == null) {
+            return causeMatcher.matches(e);
+        }
+
         for (Throwable current = e; current != null; current = 
current.getCause()) {
             if (causeMatcher.matches(current) || 
Arrays.stream(current.getSuppressed()).anyMatch(this::matchesWithCause)) {
                 return true;
diff --git 
a/modules/security/src/integrationTest/java/org/apache/ignite/internal/ssl/ItSslTest.java
 
b/modules/security/src/integrationTest/java/org/apache/ignite/internal/ssl/ItSslTest.java
index a399cdfb838..1e5107c638e 100644
--- 
a/modules/security/src/integrationTest/java/org/apache/ignite/internal/ssl/ItSslTest.java
+++ 
b/modules/security/src/integrationTest/java/org/apache/ignite/internal/ssl/ItSslTest.java
@@ -17,12 +17,14 @@
 
 package org.apache.ignite.internal.ssl;
 
+import static java.util.Collections.emptyList;
 import static 
org.apache.ignite.internal.testframework.IgniteTestUtils.escapeWindowsPath;
 import static 
org.apache.ignite.internal.testframework.IgniteTestUtils.getResourcePath;
 import static 
org.apache.ignite.internal.testframework.matchers.CompletableFutureExceptionMatcher.willThrow;
 import static 
org.apache.ignite.internal.testframework.matchers.CompletableFutureExceptionMatcher.willTimeoutIn;
 import static 
org.apache.ignite.internal.testframework.matchers.CompletableFutureMatcher.willCompleteSuccessfully;
 import static 
org.apache.ignite.jdbc.util.JdbcTestUtils.assertThrowsSqlException;
+import static 
org.apache.ignite.lang.ErrorGroups.Client.CLIENT_SSL_CONFIGURATION_ERR;
 import static org.hamcrest.MatcherAssert.assertThat;
 import static org.hamcrest.Matchers.hasSize;
 import static org.hamcrest.Matchers.is;
@@ -46,6 +48,7 @@ import org.apache.ignite.internal.Cluster;
 import org.apache.ignite.internal.Cluster.ServerRegistration;
 import org.apache.ignite.internal.ClusterConfiguration;
 import org.apache.ignite.internal.ClusterPerClassIntegrationTest;
+import org.apache.ignite.internal.IgniteExceptionTestUtils;
 import org.apache.ignite.internal.testframework.TestIgnitionManager;
 import org.apache.ignite.internal.testframework.WorkDirectory;
 import org.apache.ignite.internal.testframework.WorkDirectoryExtension;
@@ -239,11 +242,15 @@ public class ItSslTest {
                 }
             });
 
-            assertThat(ex.getMessage(), is("Client SSL configuration error: 
keystore password was incorrect"));
-            // Exceptions thrown from the synchronous build method are copied 
to include the sync method
-            assertThat(ex.getCause(), 
isA(IgniteClientConnectionException.class));
-            assertThat(ex.getCause().getCause(), isA(IOException.class));
-            assertThat(ex.getCause().getCause().getMessage(), is("keystore 
password was incorrect"));
+            assertThat(ex,
+                    IgniteExceptionTestUtils.publicException(
+                        IgniteClientConnectionException.class,
+                        CLIENT_SSL_CONFIGURATION_ERR,
+                        "Client SSL configuration error: keystore password was 
incorrect",
+                        emptyList()
+                    ).withCause(isA(IOException.class))
+            );
+            assertThat(ex.getCause().getMessage(), is("keystore password was 
incorrect"));
         }
 
         @Test
diff --git 
a/modules/sql-engine/src/integrationTest/java/org/apache/ignite/internal/sql/engine/ItPkOnlyTableCrossApiTest.java
 
b/modules/sql-engine/src/integrationTest/java/org/apache/ignite/internal/sql/engine/ItPkOnlyTableCrossApiTest.java
index 3a4032aaa2d..fa76fca0be8 100644
--- 
a/modules/sql-engine/src/integrationTest/java/org/apache/ignite/internal/sql/engine/ItPkOnlyTableCrossApiTest.java
+++ 
b/modules/sql-engine/src/integrationTest/java/org/apache/ignite/internal/sql/engine/ItPkOnlyTableCrossApiTest.java
@@ -17,12 +17,14 @@
 
 package org.apache.ignite.internal.sql.engine;
 
+import static java.util.Collections.emptyList;
+import static 
org.apache.ignite.internal.IgniteExceptionTestUtils.publicException;
 import static org.apache.ignite.internal.lang.IgniteStringFormatter.format;
 import static 
org.apache.ignite.internal.sql.engine.util.SqlTestUtils.assertThrowsSqlException;
+import static org.apache.ignite.lang.ErrorGroups.Marshalling.COMMON_ERR;
 import static org.apache.ignite.lang.ErrorGroups.Sql.CONSTRAINT_VIOLATION_ERR;
 import static org.hamcrest.MatcherAssert.assertThat;
-import static org.hamcrest.Matchers.instanceOf;
-import static org.hamcrest.Matchers.is;
+import static org.hamcrest.Matchers.any;
 import static org.junit.jupiter.api.Assertions.assertEquals;
 import static org.junit.jupiter.api.Assertions.assertFalse;
 import static org.junit.jupiter.api.Assertions.assertNotNull;
@@ -157,7 +159,8 @@ public class ItPkOnlyTableCrossApiTest extends 
BaseSqlIntegrationTest {
                 rwTx -> {
                     IgniteException ex = assertThrows(IgniteException.class,
                             () -> tab.keyValueView(KeyObject.class, 
Integer.class).put(rwTx, key, 1));
-                    assertThat(ex.getCause().getCause(), 
is(instanceOf(MarshallerException.class)));
+
+                    assertThat(ex, publicException(MarshallerException.class, 
COMMON_ERR, "", emptyList()).withMessage(any(String.class)));
 
                     kvView.put(rwTx, key, null);
 
diff --git 
a/modules/table/src/main/java/org/apache/ignite/internal/table/KeyValueViewImpl.java
 
b/modules/table/src/main/java/org/apache/ignite/internal/table/KeyValueViewImpl.java
index 6f5a91efc93..41427f90eeb 100644
--- 
a/modules/table/src/main/java/org/apache/ignite/internal/table/KeyValueViewImpl.java
+++ 
b/modules/table/src/main/java/org/apache/ignite/internal/table/KeyValueViewImpl.java
@@ -611,7 +611,11 @@ public class KeyValueViewImpl<K, V> extends 
AbstractTableView<Entry<K, V>> imple
             marsh = marshallerFactory.apply(registry.schema(schemaVersion));
             this.marsh = marsh;
         } catch (Exception ex) {
-            throw new MarshallerException(ex.getMessage(), ex);
+            if (ex instanceof MarshallerException) {
+                throw ex;
+            } else {
+                throw new MarshallerException(ex.getMessage(), ex);
+            }
         }
 
         return marsh;
diff --git 
a/modules/transactions/src/integrationTest/java/org/apache/ignite/internal/tx/ItRunInTransactionTest.java
 
b/modules/transactions/src/integrationTest/java/org/apache/ignite/internal/tx/ItRunInTransactionTest.java
index 271382da0e5..bcc9148f453 100644
--- 
a/modules/transactions/src/integrationTest/java/org/apache/ignite/internal/tx/ItRunInTransactionTest.java
+++ 
b/modules/transactions/src/integrationTest/java/org/apache/ignite/internal/tx/ItRunInTransactionTest.java
@@ -18,6 +18,7 @@
 package org.apache.ignite.internal.tx;
 
 import static java.lang.String.format;
+import static 
org.apache.ignite.internal.IgniteExceptionTestUtils.withPublicExceptionAssertions;
 import static org.apache.ignite.internal.TestWrappers.unwrapIgniteImpl;
 import static 
org.apache.ignite.internal.testframework.matchers.CompletableFutureExceptionMatcher.willThrowWithCauseOrSuppressed;
 import static 
org.apache.ignite.internal.util.CompletableFutures.nullCompletedFuture;
@@ -230,24 +231,24 @@ public class ItRunInTransactionTest extends 
ClusterPerTestIntegrationTest {
     }
 
     private static CompletableFuture<Void> putSqlAsync(Ignite client, 
Transaction tx, Tuple key) {
-        return client.sql()
+        return withPublicExceptionAssertions(client.sql())
                 .executeAsync(tx, format("INSERT INTO %s (%s, %s) VALUES (?, 
?)", TABLE_NAME, COLUMN_KEY, COLUMN_VAL), key.intValue(0),
                         key.intValue(0) + "").thenApply(r -> null);
     }
 
     private static Void putKv(Ignite client, Transaction tx, Tuple key) {
-        client.tables().tables().get(0).keyValueView().put(tx, key, 
val(key.intValue(0) + ""));
+        
withPublicExceptionAssertions(client.tables().tables().get(0).keyValueView()).put(tx,
 key, val(key.intValue(0) + ""));
         return null;
     }
 
     private static Void putSql(Ignite client, @Nullable Transaction tx, Tuple 
key) {
-        client.sql()
+        withPublicExceptionAssertions(client.sql())
                 .execute(tx, format("INSERT INTO %s (%s, %s) VALUES (?, ?)", 
TABLE_NAME, COLUMN_KEY, COLUMN_VAL), key.intValue(0),
                         key.intValue(0) + "");
         return null;
     }
 
     private static CompletableFuture<Void> putKvAsync(Ignite client, 
Transaction tx, Tuple key) {
-        return client.tables().tables().get(0).keyValueView().putAsync(tx, 
key, val(key.intValue(0) + ""));
+        return 
withPublicExceptionAssertions(client.tables().tables().get(0).keyValueView()).putAsync(tx,
 key, val(key.intValue(0) + ""));
     }
 }


Reply via email to