Repository: NomicFoundation/hardhat
Pull request #6506: feat: support stack trace warnings and infer error messages from stack trace
Author: login: galargh
Created: 2025-03-26T11:50:07Z | Merged: 2025-03-31T11:14:57Z

<!--
Thank you for using Hardhat and taking the time to send a Pull Request!

If you are introducing a new feature, please discuss it in an Issue or with someone from the team before submitting your change.

Please:
 - consider the checklist items below
 - keep the ones that make sense for your PR, and
 - DELETE the items that DON'T make sense for your PR.

## Note about small PRs and airdrop farming

We generally really appreciate external contributions, and strongly encourage meaningful additions and fixes! However, due to a recent increase in small PRs potentially created to farm airdrops, we might need to close a PR without explanation if any of the following apply:

- It is a change of very minor value that still requires additional review time/fixes (e.g. PRs fixing trivial spelling errors that can’t be merged in less than a couple of minutes due to incorrect suggestions)
- It introduces inconsequential changes (e.g. rewording phrases)
- The author of the PR does not respond in a timely manner
- We suspect the Github account of the author was created for airdrop farming
-->

- [ ] Because this PR includes a **bug fix**, relevant tests have been included.
- [ ] Because this PR includes a **new feature**, the change was previously discussed on an Issue or with someone from the team.
- [x] I didn't do anything of this.

---

This is a follow-up to https://nomicfoundation.slack.com/archives/C03P6B72ZHU/p1742980461593939

It resolved https://github.com/NomicFoundation/hardhat/issues/6505 and https://github.com/NomicFoundation/hardhat/issues/6475

Here's an example run which showcases both error messages inferred from stack trace and new stack trace warnings - https://github.com/galargh/hardhat/actions/runs/14082082256/job/39437613164

Please note that the notes are not part of the actual output.

```
...
✘ Run Failed: 443 tests, 440 passed, 3 failed, 0 skipped (duration: 12062 ms)

Failure (1): setUp()
# 💁 NOTE: This message would have been previously EvmError: Revert
Reason: Transaction reverted without a reason string
  at TokenLocker.constructor (test/vault/VaultReentrancy.t.sol:22)
  at TokenLocker.<unknown> (test/vault/VaultReentrancy.t.sol:18)

Failure (2): test_getTickAtSqrtRatio_matchesJavascriptImplWithin1()
Reason: EvmError: Revert
# 💁 NOTE: This is a new entry in the test failure details
Stack Trace Warning: The test is not safe to replay because it uses impure cheatcodes: function ffi(string[] calldata commandInput) external returns (bytes memory result);

Failure (3): test_getSqrtRatioAtTick_matchesJavaScriptImplByOneHundrethOfABip()
Reason: EvmError: Revert
Stack Trace Warning: The test is not safe to replay because it uses impure cheatcodes: function ffi(string[] calldata commandInput) external returns (bytes memory result);
```



Diff:
diff --git a/.changeset/pretty-lizards-roll.md b/.changeset/pretty-lizards-roll.md
new file mode 100644
index 00000000000..e53654ccd9d
--- /dev/null
+++ b/.changeset/pretty-lizards-roll.md
@@ -0,0 +1,5 @@
+---
+"hardhat": patch
+---
+
+Infer and display better error messages from stack traces ([#6505](https://github.com/NomicFoundation/hardhat/issues/6505))
diff --git a/v-next/hardhat/src/internal/builtin-plugins/solidity-test/reporter.ts b/v-next/hardhat/src/internal/builtin-plugins/solidity-test/reporter.ts
index 554657db3d3..90c0e4f5fea 100644
--- a/v-next/hardhat/src/internal/builtin-plugins/solidity-test/reporter.ts
+++ b/v-next/hardhat/src/internal/builtin-plugins/solidity-test/reporter.ts
@@ -11,6 +11,7 @@ import chalk from "chalk";
 import { encodeStackTraceEntry } from "../network-manager/edr/stack-traces/stack-trace-solidity-errors.js";
 
 import { formatArtifactId } from "./formatters.js";
+import { getMessageFromLastStackTraceEntry } from "./stack-trace-solidity-errors.js";
 
 /**
  * This is a solidity test reporter. It is intended to be composed with the
@@ -119,33 +120,48 @@ export async function* testReporter(
     for (const [index, failure] of failures.entries()) {
       yield `\n${chalk.bold(chalk.red(`Failure (${index + 1})`))}: ${failure.name}\n`;
 
-      if (
-        failure.reason !== undefined &&
-        failure.reason !== null &&
-        failure.reason !== ""
-      ) {
-        yield `Reason: ${chalk.grey(failure.reason)}\n`;
+      const stackTrace = failure.stackTrace();
+
+      let reason: string | undefined;
+      if (stackTrace?.kind === "StackTrace") {
+        reason = getMessageFromLastStackTraceEntry(
+          stackTrace.entries[stackTrace.entries.length - 1],
+        );
+      }
+      if (reason === undefined || reason === "") {
+        reason = failure.reason ?? "Unknown error";
       }
 
-      const stackTrace = failure.stackTrace();
-      // TODO handle `UnexpectedError` and `UnsafeToReplay` variants
-      if (
-        stackTrace !== undefined &&
-        stackTrace !== null &&
-        stackTrace.kind === "StackTrace" &&
-        stackTrace.entries.length > 0
-      ) {
-        const stackTraceStack: string[] = [];
-        for (const entry of stackTrace.entries.reverse()) {
-          const callsite = encodeStackTraceEntry(entry);
-          if (callsite !== undefined) {
-            stackTraceStack.push(`  at ${callsite.toString()}`);
+      yield `Reason: ${chalk.grey(reason)}\n`;
+
+      switch (stackTrace?.kind) {
+        case "StackTrace":
+          const stackTraceStack: string[] = [];
+          for (const entry of stackTrace.entries.reverse()) {
+            const callsite = encodeStackTraceEntry(entry);
+            if (callsite !== undefined) {
+              stackTraceStack.push(`  at ${callsite.toString()}`);
+            }
           }
-        }
 
-        if (stackTraceStack.length > 0) {
-          yield `${chalk.grey(stackTraceStack.join("\n"))}\n`;
-        }
+          if (stackTraceStack.length > 0) {
+            yield `${chalk.grey(stackTraceStack.join("\n"))}\n`;
+          }
+          break;
+        case "UnexpectedError":
+          yield `Stack Trace Warning: ${chalk.grey(stackTrace.errorMessage)}\n`;
+          break;
+        case "UnsafeToReplay":
+          if (stackTrace.globalForkLatest === true) {
+            yield `Stack Trace Warning: ${chalk.grey("The test is not safe to replay because a fork url without a fork block number was provided.")}\n`;
+          }
+          if (stackTrace.impureCheatcodes.length > 0) {
+            yield `Stack Trace Warning: ${chalk.grey(`The test is not safe to replay because it uses impure cheatcodes: ${stackTrace.impureCheatcodes.join(", ")}`)}\n`;
+          }
+          break;
+        case "HeuristicFailed":
+        default:
+          break;
       }
 
       if (
diff --git a/v-next/hardhat/src/internal/builtin-plugins/solidity-test/stack-trace-solidity-errors.ts b/v-next/hardhat/src/internal/builtin-plugins/solidity-test/stack-trace-solidity-errors.ts
new file mode 100644
index 00000000000..553d4c0b5bb
--- /dev/null
+++ b/v-next/hardhat/src/internal/builtin-plugins/solidity-test/stack-trace-solidity-errors.ts
@@ -0,0 +1,83 @@
+import type { SolidityStackTraceEntry } from "../network-manager/edr/stack-traces/solidity-stack-trace.js";
+
+import { panicErrorCodeToMessage } from "../network-manager/edr/stack-traces/panic-errors.js";
+import { StackTraceEntryType } from "../network-manager/edr/stack-traces/solidity-stack-trace.js";
+
+export function getMessageFromLastStackTraceEntry(
+  stackTraceEntry: SolidityStackTraceEntry,
+): string | undefined {
+  switch (stackTraceEntry.type) {
+    case StackTraceEntryType.PRECOMPILE_ERROR:
+      return `Call to precompile ${stackTraceEntry.precompile} failed`;
+
+    case StackTraceEntryType.FUNCTION_NOT_PAYABLE_ERROR:
+      return `Non-payable function was called with value ${stackTraceEntry.value.toString(
+        10,
+      )}`;
+
+    case StackTraceEntryType.INVALID_PARAMS_ERROR:
+      return `Function was called with incorrect parameters`;
+
+    case StackTraceEntryType.FALLBACK_NOT_PAYABLE_ERROR:
+      return `Fallback function is not payable and was called with value ${stackTraceEntry.value.toString(
+        10,
+      )}`;
+
+    case StackTraceEntryType.FALLBACK_NOT_PAYABLE_AND_NO_RECEIVE_ERROR:
+      return `There's no receive function, fallback function is not payable and was called with value ${stackTraceEntry.value.toString(
+        10,
+      )}`;
+
+    case StackTraceEntryType.UNRECOGNIZED_FUNCTION_WITHOUT_FALLBACK_ERROR:
+      return `Function selector was not recognized and there's no fallback function`;
+
+    case StackTraceEntryType.MISSING_FALLBACK_OR_RECEIVE_ERROR:
+      return `Function selector was not recognized and there's no fallback nor receive function`;
+
+    case StackTraceEntryType.RETURNDATA_SIZE_ERROR:
+      return `Function returned an unexpected amount of data`;
+
+    case StackTraceEntryType.NONCONTRACT_ACCOUNT_CALLED_ERROR:
+      return `Function call to a non-contract account`;
+
+    case StackTraceEntryType.CALL_FAILED_ERROR:
+      return `Function call failed to execute`;
+
+    case StackTraceEntryType.DIRECT_LIBRARY_CALL_ERROR:
+      return `Library was called directly`;
+
+    case StackTraceEntryType.UNRECOGNIZED_CREATE_ERROR:
+    case StackTraceEntryType.UNRECOGNIZED_CONTRACT_ERROR: {
+      return "Contract reverted without a reason string";
+    }
+
+    case StackTraceEntryType.REVERT_ERROR: {
+      return "Contract reverted without a reason string";
+    }
+
+    case StackTraceEntryType.PANIC_ERROR:
+      return panicErrorCodeToMessage(stackTraceEntry.errorCode);
+
+    case StackTraceEntryType.CUSTOM_ERROR:
+      return stackTraceEntry.message;
+
+    case StackTraceEntryType.CONTRACT_TOO_LARGE_ERROR:
+      return "Trying to deploy a contract whose code is too large";
+
+    case StackTraceEntryType.CONTRACT_CALL_RUN_OUT_OF_GAS_ERROR:
+      return "Contract call run out of gas";
+
+    /* These types are not associate with a more detailed error message */
+    case StackTraceEntryType.UNMAPPED_SOLC_0_6_3_REVERT_ERROR:
+    case StackTraceEntryType.OTHER_EXECUTION_ERROR:
+      return undefined;
+
+    /* These types are not expected to be the last entry in the stack trace, as
+    their presence indicates that another frame should follow in the call stack. */
+    case StackTraceEntryType.CALLSTACK_ENTRY:
+    case StackTraceEntryType.UNRECOGNIZED_CREATE_CALLSTACK_ENTRY:
+    case StackTraceEntryType.UNRECOGNIZED_CONTRACT_CALLSTACK_ENTRY:
+    case StackTraceEntryType.INTERNAL_FUNCTION_CALLSTACK_ENTRY:
+      return undefined;
+  }
+}


Reviews:
user: login: agostbiro
body: Thanks, LGTM, but I made two comments where other reviewers might have different conclusions.
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-03-26T12:09:14Z
user: login: galargh
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-26T12:16:51Z
user: login: galargh
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-26T12:40:27Z
user: login: galargh
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-26T15:09:48Z
user: login: kanej
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-27T09:58:49Z
user: login: alcuadrado
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-27T21:20:23Z
user: login: alcuadrado
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-27T21:21:03Z
user: login: alcuadrado
body: I left a few questions, but in general this looks good to me
state: APPROVED
author_association: MEMBER
submitted_at: 2025-03-27T21:21:24Z
user: login: galargh
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-31T11:08:44Z
user: login: galargh
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-31T11:09:18Z

Review comments:
diff_hunk: @@ -119,33 +122,46 @@ export async function* testReporter(
     for (const [index, failure] of failures.entries()) {
       yield `\n${chalk.bold(chalk.red(`Failure (${index + 1})`))}: ${failure.name}\n`;
 
-      if (
-        failure.reason !== undefined &&
-        failure.reason !== null &&
-        failure.reason !== ""
-      ) {
-        yield `Reason: ${chalk.grey(failure.reason)}\n`;
+      const stackTrace = failure.stackTrace();
+
+      let reason: string | undefined;
+      if (stackTrace?.kind === "StackTrace") {
+        reason = getMessageFromLastStackTraceEntry(stackTrace.entries[stackTrace.entries.length - 1]);
+      }
+      if (reason === undefined || reason === "") {
+        reason = failure.reason ?? "Unknown error";
       }
 
-      const stackTrace = failure.stackTrace();
-      // TODO handle `UnexpectedError` and `UnsafeToReplay` variants
-      if (
-        stackTrace !== undefined &&
-        stackTrace !== null &&
-        stackTrace.kind === "StackTrace" &&
-        stackTrace.entries.length > 0
-      ) {
-        const stackTraceStack: string[] = [];
-        for (const entry of stackTrace.entries.reverse()) {
-          const callsite = encodeStackTraceEntry(entry);
-          if (callsite !== undefined) {
-            stackTraceStack.push(`  at ${callsite.toString()}`);
+      yield `Reason: ${chalk.grey(reason)}\n`;
+
+      switch (stackTrace?.kind) {
+        case "StackTrace":
+          const stackTraceStack: string[] = [];
+          for (const entry of stackTrace.entries.reverse()) {
+            const callsite = encodeStackTraceEntry(entry);
+            if (callsite !== undefined) {
+              stackTraceStack.push(`  at ${callsite.toString()}`);
+            }
           }
-        }
 
-        if (stackTraceStack.length > 0) {
-          yield `${chalk.grey(stackTraceStack.join("\n"))}\n`;
-        }
+          if (stackTraceStack.length > 0) {
+            yield `${chalk.grey(stackTraceStack.join("\n"))}\n`;
+          }
+          break;
+        case "UnexpectedError":
+          yield `Stack Trace Warning: ${chalk.grey(stackTrace.errorMessage)}\n`;
path: v-next/hardhat/src/internal/builtin-plugins/solidity-test/reporter.ts
user: login: agostbiro
body: This used to throw before. I think it's ok to turn it into a warning, but wanted to make sure we're all on the same page about this.
created_at: 2025-03-26T12:05:51Z
author_association: CONTRIBUTOR
diff_hunk: @@ -119,33 +122,46 @@ export async function* testReporter(
     for (const [index, failure] of failures.entries()) {
       yield `\n${chalk.bold(chalk.red(`Failure (${index + 1})`))}: ${failure.name}\n`;
 
-      if (
-        failure.reason !== undefined &&
-        failure.reason !== null &&
-        failure.reason !== ""
-      ) {
-        yield `Reason: ${chalk.grey(failure.reason)}\n`;
+      const stackTrace = failure.stackTrace();
+
+      let reason: string | undefined;
+      if (stackTrace?.kind === "StackTrace") {
+        reason = getMessageFromLastStackTraceEntry(stackTrace.entries[stackTrace.entries.length - 1]);
path: v-next/hardhat/src/internal/builtin-plugins/solidity-test/reporter.ts
user: login: agostbiro
body: The messages from `getMessageFromLastStackTraceEntry` are written with a Hardhat Network use-case in mind and they can be a bit weird in a Solidity-testing context. There is more discussion of this here: https://github.com/NomicFoundation/hardhat/pull/6235#discussion_r1946455490

For me it's ok to merge like this, but wanted to make sure we discuss this (cc @fvictorio )
created_at: 2025-03-26T12:08:39Z
author_association: CONTRIBUTOR
diff_hunk: @@ -119,33 +122,46 @@ export async function* testReporter(
     for (const [index, failure] of failures.entries()) {
       yield `\n${chalk.bold(chalk.red(`Failure (${index + 1})`))}: ${failure.name}\n`;
 
-      if (
-        failure.reason !== undefined &&
-        failure.reason !== null &&
-        failure.reason !== ""
-      ) {
-        yield `Reason: ${chalk.grey(failure.reason)}\n`;
+      const stackTrace = failure.stackTrace();
+
+      let reason: string | undefined;
+      if (stackTrace?.kind === "StackTrace") {
+        reason = getMessageFromLastStackTraceEntry(stackTrace.entries[stackTrace.entries.length - 1]);
path: v-next/hardhat/src/internal/builtin-plugins/solidity-test/reporter.ts
user: login: galargh
body: Ah, I forgot about that! Thank you! As suggested in the original thread, I think I'm going to duplicate `getMessageFromLastStackTraceEntry` and run the first iteration of message polishing. We can either wait for that to be complete or merge as-is and update the messaging as a follow-up.
created_at: 2025-03-26T12:16:51Z
author_association: CONTRIBUTOR
diff_hunk: @@ -119,33 +122,46 @@ export async function* testReporter(
     for (const [index, failure] of failures.entries()) {
       yield `\n${chalk.bold(chalk.red(`Failure (${index + 1})`))}: ${failure.name}\n`;
 
-      if (
-        failure.reason !== undefined &&
-        failure.reason !== null &&
-        failure.reason !== ""
-      ) {
-        yield `Reason: ${chalk.grey(failure.reason)}\n`;
+      const stackTrace = failure.stackTrace();
+
+      let reason: string | undefined;
+      if (stackTrace?.kind === "StackTrace") {
+        reason = getMessageFromLastStackTraceEntry(stackTrace.entries[stackTrace.entries.length - 1]);
path: v-next/hardhat/src/internal/builtin-plugins/solidity-test/reporter.ts
user: login: galargh
body: I quickly created a duplicate of the original message inference function in solidity-test plugin and run a first pass at the error message simplification. I haven't yet fully analysed which even make sense in the context of solidiy-tests and which don't but I wanted to put it out there as quickly as possible because I think there might be value into opening up these deliberations to a wider audience. Here's the commit in question - https://github.com/NomicFoundation/hardhat/pull/6506/commits/543ea3c6862dfb1ad934325e2fb172d216e57809
created_at: 2025-03-26T12:40:27Z
author_association: CONTRIBUTOR
diff_hunk: @@ -119,33 +120,48 @@ export async function* testReporter(
     for (const [index, failure] of failures.entries()) {
       yield `\n${chalk.bold(chalk.red(`Failure (${index + 1})`))}: ${failure.name}\n`;
 
-      if (
-        failure.reason !== undefined &&
-        failure.reason !== null &&
-        failure.reason !== ""
-      ) {
-        yield `Reason: ${chalk.grey(failure.reason)}\n`;
+      const stackTrace = failure.stackTrace();
+
+      let reason: string | undefined;
+      if (stackTrace?.kind === "StackTrace") {
+        reason = getMessageFromLastStackTraceEntry(
+          stackTrace.entries[stackTrace.entries.length - 1],
+        );
+      }
+      if (reason === undefined || reason === "") {
+        reason = failure.reason ?? "Unknown error";
       }
path: v-next/hardhat/src/internal/builtin-plugins/solidity-test/reporter.ts
user: login: galargh
body: One more thing worth pointing out here is that this changes the behaviour of what error message we display to the user. After this is merged, we'll firstly try to extract the detailed error message from stack trace and only if that fails we'll fall back to the `failure.reason` if it exists.
created_at: 2025-03-26T15:09:47Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,5 @@
+---
+"hardhat": patch
+---
+
+Display solidity stack trace generation warnings to the users and try to infer better error messages from said stack traces
path: .changeset/pretty-lizards-roll.md
user: login: kanej
body: ```suggestion
Infer and display better error messages from stack traces ([#6505](https://github.com/NomicFoundation/hardhat/issues/6505))
```
created_at: 2025-03-27T09:58:49Z
author_association: MEMBER
diff_hunk: @@ -119,33 +122,46 @@ export async function* testReporter(
     for (const [index, failure] of failures.entries()) {
       yield `\n${chalk.bold(chalk.red(`Failure (${index + 1})`))}: ${failure.name}\n`;
 
-      if (
-        failure.reason !== undefined &&
-        failure.reason !== null &&
-        failure.reason !== ""
-      ) {
-        yield `Reason: ${chalk.grey(failure.reason)}\n`;
+      const stackTrace = failure.stackTrace();
+
+      let reason: string | undefined;
+      if (stackTrace?.kind === "StackTrace") {
+        reason = getMessageFromLastStackTraceEntry(stackTrace.entries[stackTrace.entries.length - 1]);
+      }
+      if (reason === undefined || reason === "") {
+        reason = failure.reason ?? "Unknown error";
       }
 
-      const stackTrace = failure.stackTrace();
-      // TODO handle `UnexpectedError` and `UnsafeToReplay` variants
-      if (
-        stackTrace !== undefined &&
-        stackTrace !== null &&
-        stackTrace.kind === "StackTrace" &&
-        stackTrace.entries.length > 0
-      ) {
-        const stackTraceStack: string[] = [];
-        for (const entry of stackTrace.entries.reverse()) {
-          const callsite = encodeStackTraceEntry(entry);
-          if (callsite !== undefined) {
-            stackTraceStack.push(`  at ${callsite.toString()}`);
+      yield `Reason: ${chalk.grey(reason)}\n`;
+
+      switch (stackTrace?.kind) {
+        case "StackTrace":
+          const stackTraceStack: string[] = [];
+          for (const entry of stackTrace.entries.reverse()) {
+            const callsite = encodeStackTraceEntry(entry);
+            if (callsite !== undefined) {
+              stackTraceStack.push(`  at ${callsite.toString()}`);
+            }
           }
-        }
 
-        if (stackTraceStack.length > 0) {
-          yield `${chalk.grey(stackTraceStack.join("\n"))}\n`;
-        }
+          if (stackTraceStack.length > 0) {
+            yield `${chalk.grey(stackTraceStack.join("\n"))}\n`;
+          }
+          break;
+        case "UnexpectedError":
+          yield `Stack Trace Warning: ${chalk.grey(stackTrace.errorMessage)}\n`;
path: v-next/hardhat/src/internal/builtin-plugins/solidity-test/reporter.ts
user: login: alcuadrado
body: Is this an unexpected error in the stack traces generation? Or when? Maybe the name can be more explicit.
created_at: 2025-03-27T21:20:23Z
author_association: MEMBER
diff_hunk: @@ -119,33 +120,48 @@ export async function* testReporter(
     for (const [index, failure] of failures.entries()) {
       yield `\n${chalk.bold(chalk.red(`Failure (${index + 1})`))}: ${failure.name}\n`;
 
-      if (
-        failure.reason !== undefined &&
-        failure.reason !== null &&
-        failure.reason !== ""
-      ) {
-        yield `Reason: ${chalk.grey(failure.reason)}\n`;
+      const stackTrace = failure.stackTrace();
+
+      let reason: string | undefined;
+      if (stackTrace?.kind === "StackTrace") {
+        reason = getMessageFromLastStackTraceEntry(
+          stackTrace.entries[stackTrace.entries.length - 1],
+        );
+      }
+      if (reason === undefined || reason === "") {
+        reason = failure.reason ?? "Unknown error";
       }
 
-      const stackTrace = failure.stackTrace();
-      // TODO handle `UnexpectedError` and `UnsafeToReplay` variants
-      if (
-        stackTrace !== undefined &&
-        stackTrace !== null &&
-        stackTrace.kind === "StackTrace" &&
-        stackTrace.entries.length > 0
-      ) {
-        const stackTraceStack: string[] = [];
-        for (const entry of stackTrace.entries.reverse()) {
-          const callsite = encodeStackTraceEntry(entry);
-          if (callsite !== undefined) {
-            stackTraceStack.push(`  at ${callsite.toString()}`);
+      yield `Reason: ${chalk.grey(reason)}\n`;
+
+      switch (stackTrace?.kind) {
+        case "StackTrace":
+          const stackTraceStack: string[] = [];
+          for (const entry of stackTrace.entries.reverse()) {
+            const callsite = encodeStackTraceEntry(entry);
+            if (callsite !== undefined) {
+              stackTraceStack.push(`  at ${callsite.toString()}`);
+            }
           }
-        }
 
-        if (stackTraceStack.length > 0) {
-          yield `${chalk.grey(stackTraceStack.join("\n"))}\n`;
-        }
+          if (stackTraceStack.length > 0) {
+            yield `${chalk.grey(stackTraceStack.join("\n"))}\n`;
+          }
+          break;
+        case "UnexpectedError":
+          yield `Stack Trace Warning: ${chalk.grey(stackTrace.errorMessage)}\n`;
+          break;
+        case "UnsafeToReplay":
+          if (stackTrace.globalForkLatest === true) {
+            yield `Stack Trace Warning: ${chalk.grey("The test is not safe to replay because a fork url without a fork block number was provided.")}\n`;
+          }
+          if (stackTrace.impureCheatcodes.length > 0) {
+            yield `Stack Trace Warning: ${chalk.grey(`The test is not safe to replay because it uses impure cheatcodes: ${stackTrace.impureCheatcodes.join(", ")}`)}\n`;
+          }
+          break;
+        case "HeuristicFailed":
path: v-next/hardhat/src/internal/builtin-plugins/solidity-test/reporter.ts
user: login: alcuadrado
body: What does this mean, @agostbiro? Does it make sense to just ignore it? How is it different than `UnexpectedError`?
created_at: 2025-03-27T21:21:03Z
author_association: MEMBER
diff_hunk: @@ -119,33 +120,48 @@ export async function* testReporter(
     for (const [index, failure] of failures.entries()) {
       yield `\n${chalk.bold(chalk.red(`Failure (${index + 1})`))}: ${failure.name}\n`;
 
-      if (
-        failure.reason !== undefined &&
-        failure.reason !== null &&
-        failure.reason !== ""
-      ) {
-        yield `Reason: ${chalk.grey(failure.reason)}\n`;
+      const stackTrace = failure.stackTrace();
+
+      let reason: string | undefined;
+      if (stackTrace?.kind === "StackTrace") {
+        reason = getMessageFromLastStackTraceEntry(
+          stackTrace.entries[stackTrace.entries.length - 1],
+        );
+      }
+      if (reason === undefined || reason === "") {
+        reason = failure.reason ?? "Unknown error";
       }
 
-      const stackTrace = failure.stackTrace();
-      // TODO handle `UnexpectedError` and `UnsafeToReplay` variants
-      if (
-        stackTrace !== undefined &&
-        stackTrace !== null &&
-        stackTrace.kind === "StackTrace" &&
-        stackTrace.entries.length > 0
-      ) {
-        const stackTraceStack: string[] = [];
-        for (const entry of stackTrace.entries.reverse()) {
-          const callsite = encodeStackTraceEntry(entry);
-          if (callsite !== undefined) {
-            stackTraceStack.push(`  at ${callsite.toString()}`);
+      yield `Reason: ${chalk.grey(reason)}\n`;
+
+      switch (stackTrace?.kind) {
+        case "StackTrace":
+          const stackTraceStack: string[] = [];
+          for (const entry of stackTrace.entries.reverse()) {
+            const callsite = encodeStackTraceEntry(entry);
+            if (callsite !== undefined) {
+              stackTraceStack.push(`  at ${callsite.toString()}`);
+            }
           }
-        }
 
-        if (stackTraceStack.length > 0) {
-          yield `${chalk.grey(stackTraceStack.join("\n"))}\n`;
-        }
+          if (stackTraceStack.length > 0) {
+            yield `${chalk.grey(stackTraceStack.join("\n"))}\n`;
+          }
+          break;
+        case "UnexpectedError":
+          yield `Stack Trace Warning: ${chalk.grey(stackTrace.errorMessage)}\n`;
+          break;
+        case "UnsafeToReplay":
+          if (stackTrace.globalForkLatest === true) {
+            yield `Stack Trace Warning: ${chalk.grey("The test is not safe to replay because a fork url without a fork block number was provided.")}\n`;
+          }
+          if (stackTrace.impureCheatcodes.length > 0) {
+            yield `Stack Trace Warning: ${chalk.grey(`The test is not safe to replay because it uses impure cheatcodes: ${stackTrace.impureCheatcodes.join(", ")}`)}\n`;
+          }
+          break;
+        case "HeuristicFailed":
path: v-next/hardhat/src/internal/builtin-plugins/solidity-test/reporter.ts
user: login: galargh
body: We get `HeuristicFailed` when the process of generating stack trace goes through, but the stack trace is empty i.e. we couldn't find any useful information. We don't get any additional information on why that might have been though. We could inform the user about it but I'm not sure if there's anything actionable on their part we could display.

`UnexpectedError`, on the other hand, happens when the stack trace generation throws.
created_at: 2025-03-31T11:08:44Z
author_association: CONTRIBUTOR
diff_hunk: @@ -119,33 +122,46 @@ export async function* testReporter(
     for (const [index, failure] of failures.entries()) {
       yield `\n${chalk.bold(chalk.red(`Failure (${index + 1})`))}: ${failure.name}\n`;
 
-      if (
-        failure.reason !== undefined &&
-        failure.reason !== null &&
-        failure.reason !== ""
-      ) {
-        yield `Reason: ${chalk.grey(failure.reason)}\n`;
+      const stackTrace = failure.stackTrace();
+
+      let reason: string | undefined;
+      if (stackTrace?.kind === "StackTrace") {
+        reason = getMessageFromLastStackTraceEntry(stackTrace.entries[stackTrace.entries.length - 1]);
+      }
+      if (reason === undefined || reason === "") {
+        reason = failure.reason ?? "Unknown error";
       }
 
-      const stackTrace = failure.stackTrace();
-      // TODO handle `UnexpectedError` and `UnsafeToReplay` variants
-      if (
-        stackTrace !== undefined &&
-        stackTrace !== null &&
-        stackTrace.kind === "StackTrace" &&
-        stackTrace.entries.length > 0
-      ) {
-        const stackTraceStack: string[] = [];
-        for (const entry of stackTrace.entries.reverse()) {
-          const callsite = encodeStackTraceEntry(entry);
-          if (callsite !== undefined) {
-            stackTraceStack.push(`  at ${callsite.toString()}`);
+      yield `Reason: ${chalk.grey(reason)}\n`;
+
+      switch (stackTrace?.kind) {
+        case "StackTrace":
+          const stackTraceStack: string[] = [];
+          for (const entry of stackTrace.entries.reverse()) {
+            const callsite = encodeStackTraceEntry(entry);
+            if (callsite !== undefined) {
+              stackTraceStack.push(`  at ${callsite.toString()}`);
+            }
           }
-        }
 
-        if (stackTraceStack.length > 0) {
-          yield `${chalk.grey(stackTraceStack.join("\n"))}\n`;
-        }
+          if (stackTraceStack.length > 0) {
+            yield `${chalk.grey(stackTraceStack.join("\n"))}\n`;
+          }
+          break;
+        case "UnexpectedError":
+          yield `Stack Trace Warning: ${chalk.grey(stackTrace.errorMessage)}\n`;
path: v-next/hardhat/src/internal/builtin-plugins/solidity-test/reporter.ts
user: login: galargh
body: Yes, that's during stack trace generation. 
created_at: 2025-03-31T11:09:18Z
author_association: CONTRIBUTOR

PR comments:
user: login: changeset-bot[bot]
created_at: 2025-03-26T11:50:12Z
body: ###  🦋  Changeset detected

Latest commit: 402b29121c190adb450f2f5aeee9707cee2f3f0e

**The changes in this PR will be included in the next version bump.**

<details><summary>This PR includes changesets to release 1 package</summary>

  | Name    | Type  |
| ------- | ----- |
| hardhat | Patch |

</details>

Not sure what this means? [Click here  to learn what changesets are](https://github.com/changesets/changesets/blob/main/docs/adding-a-changeset.md).

[Click here if you're a maintainer who wants to add another changeset to this PR](https://github.com/NomicFoundation/hardhat/new/solidity-test-messages?filename=.changeset/honest-icons-hang.md&value=---%0A%22hardhat%22%3A%20patch%0A---%0A%0Afeat%3A%20support%20stack%20trace%20warnings%20and%20infer%20error%20messages%20from%20stack%20trace%0A)


author_association: NONE
user: login: vercel[bot]
created_at: 2025-03-26T11:50:13Z
body: [vc]: #yLYLdizAbI2ZlApEwR3ohMbtCMSvxGZlbAcSltO9frc=:eyJpc01vbm9yZXBvIjp0cnVlLCJ0eXBlIjoiZ2l0aHViIiwicHJvamVjdHMiOlt7Im5hbWUiOiJoYXJkaGF0IiwibGl2ZUZlZWRiYWNrIjp7InJlc29sdmVkIjowLCJ1bnJlc29sdmVkIjowLCJ0b3RhbCI6MCwibGluayI6ImhhcmRoYXQtZ2l0LXNvbGlkaXR5LXRlc3QtbWVzc2FnZXMtbm9taWMtZm91bmRhdGlvbi52ZXJjZWwuYXBwIn0sImluc3BlY3RvclVybCI6Imh0dHBzOi8vdmVyY2VsLmNvbS9ub21pYy1mb3VuZGF0aW9uL2hhcmRoYXQvQjdvZndzNHlOOHByOTdWelpiSnRNb1BDemZGaCIsInByZXZpZXdVcmwiOiJoYXJkaGF0LWdpdC1zb2xpZGl0eS10ZXN0LW1lc3NhZ2VzLW5vbWljLWZvdW5kYXRpb24udmVyY2VsLmFwcCIsIm5leHRDb21taXRTdGF0dXMiOiJERVBMT1lFRCIsInJvb3REaXJlY3RvcnkiOiJkb2NzIn1dfQ==
**The latest updates on your projects**. Learn more about [Vercel for Git ↗︎](https://vercel.link/github-learn-more)

| Name | Status | Preview | Comments | Updated (UTC) |
| :--- | :----- | :------ | :------- | :------ |
| **hardhat** | ✅ Ready ([Inspect](https://vercel.com/nomic-foundation/hardhat/B7ofws4yN8pr97VzZbJtMoPCzfFh)) | [Visit Preview](https://hardhat-git-solidity-test-messages-nomic-foundation.vercel.app) | 💬 [**Add feedback**](https://vercel.live/open-feedback/hardhat-git-solidity-test-messages-nomic-foundation.vercel.app?via=pr-comment-feedback-link) | Mar 27, 2025 10:33am |


author_association: NONE
user: login: fvictorio
created_at: 2025-03-27T16:30:25Z
body: I only skimmed the code, but I tested this with the same example I used in #6505 and now it works nicely!

```
Failure (1): test()
Reason: Function call to a non-contract account
  at Foo.f (contracts/Foo.sol:10)
  at FooTest.test (contracts/Foo.t.sol:9)
```
author_association: CONTRIBUTOR