You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@texera.apache.org by gi...@apache.org on 2026/07/10 01:02:37 UTC

(texera) branch gh-readonly-queue/main/pr-6288-1e57801335bf7a2459546aeb4c036a0374b455c4 created (now 5a2c2ab982)

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

github-merge-queue[bot] pushed a change to branch gh-readonly-queue/main/pr-6288-1e57801335bf7a2459546aeb4c036a0374b455c4
in repository https://gitbox.apache.org/repos/asf/texera.git


      at 5a2c2ab982 fix(frontend): improve static error msgs to be user-friendly for unco… (#6288)

This branch includes the following new commits:

     new 5a2c2ab982 fix(frontend): improve static error msgs to be user-friendly for unco… (#6288)

The 1 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails.  The revisions
listed as "add" were already present in the repository and have only
been added to this reference.



(texera) 01/01: fix(frontend): improve static error msgs to be user-friendly for unco… (#6288)

Posted by gi...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

github-merge-queue[bot] pushed a commit to branch gh-readonly-queue/main/pr-6288-1e57801335bf7a2459546aeb4c036a0374b455c4
in repository https://gitbox.apache.org/repos/asf/texera.git

commit 5a2c2ab982c3983b92b29d80502e0d63d2a5a496
Author: roshiiiiz <ro...@gmail.com>
AuthorDate: Fri Jul 10 06:02:15 2026 +0500

    fix(frontend): improve static error msgs to be user-friendly for unco… (#6288)
    
    ### What changes were proposed in this PR?
    This PR overhauls the static error validation and error messaging across
    Texera operators to drastically improve the experience for new users
    during workflow editing.
    
    Previously, if a user dragged an operator (like `Projection`, `MySQL`,
    or `CSV File Scan`) onto the canvas without configuring it yet, the
    system would immediately throw intimidating and cryptic Java exceptions
    (e.g., `IllegalArgumentException`, `SQLException: No suitable driver
    found`, or `/texera (Is a directory)`).
    
    Changes proposed:
    1. **Frontend Global Catch-all**: Updated the regex logic in
    `error-frame.component.ts` to cleanly strip internal Java prefixes (like
    `java.lang.IllegalArgumentException:` or `requirement failed:`) so the
    user only sees the human-readable part of the error message.
    2. **Projection Operator**: Updated `ProjectionOpDesc` to explicitly
    prompt the user to select an attribute.
    3. **Database Sources**: Updated `SQLSourceOpDesc` (and
    `AsterixDBSourceOpDesc`) to intercept empty database configuration
    fields and return actionable UI instructions (e.g., "Please enter a
    valid host name for the database in the properties panel").
    4. **File Scan Sources**: Updated all file scan operators
    (`ParallelCSV`, `JSONL`, `Arrow`, etc.) to gracefully catch directory
    selections and missing files before attempting physical reads, replacing
    IOExceptions with clear prompts to select a valid file from the
    right-hand panel.
    
    ### Any related issues, documentation, discussions?
    Issue#2735
    
    ### How was this PR tested?
    Tested manually by rebuilding the `workflow-compiling-service` and
    dragging every modified operator (Projection, MySQL, PostgreSQL,
    AsterixDB, CSV, JSONL, Arrow) onto a new workflow canvas. Verified that
    the Result Panel correctly displays the newly formatted, user-friendly
    instructions instead of raw Java exceptions.
    
    ### Was this PR authored or co-authored using generative AI tooling?
    Generated-by: Antigravity (DeepMind)
    
    **Tests:**
    <img width="890" height="560" alt="WhatsApp Image 2026-07-09 at 12 56
    49"
    src="https://github.com/user-attachments/assets/4dd8f144-49c4-439f-be9b-89fbbfc6f8dd"
    />
    
    --------------------------------------------------------------------------------------------------------------------------------------
    <img width="1143" height="811" alt="image"
    src="https://github.com/user-attachments/assets/463ec68b-636c-4ff9-ba65-f907f229c79d"
    />
    
    --------------------------------------------------------------------------------------------------------------------------------------
    <img width="1544" height="639" alt="image"
    src="https://github.com/user-attachments/assets/923bc582-d825-4b15-8b00-6fff1f651642"
    />
    
    --------------------------------------------------------------------------------------------------------------------------------------
    <img width="1397" height="709" alt="image"
    src="https://github.com/user-attachments/assets/731510a0-fba1-4a0f-86fd-12b2766e8e17"
    />
    
    --------------------------------------------------------------------------------------------------------------------------------------
    
    ---------
    
    Co-authored-by: probe <pr...@x>
---
 .../org/apache/texera/workflow/LogicalPlan.scala   |  6 ++++-
 .../texera/workflow/WorkflowCompilerSpec.scala     |  4 +--
 .../operator/projection/ProjectionOpDesc.scala     |  2 +-
 .../source/scan/arrow/ArrowSourceOpDesc.scala      | 26 ++++++++++++++++---
 .../source/scan/csv/CSVScanSourceOpDesc.scala      | 18 ++++++++++---
 .../scan/csv/ParallelCSVScanSourceOpDesc.scala     | 18 ++++++++++---
 .../scan/csvOld/CSVOldScanSourceOpDesc.scala       | 19 ++++++++++----
 .../source/scan/json/JSONLScanSourceOpDesc.scala   | 15 ++++++++---
 .../operator/source/sql/SQLSourceOpDesc.scala      | 30 +++++++++++++++++-----
 .../sql/asterixdb/AsterixDBSourceOpDesc.scala      | 10 +++++---
 .../source/scan/arrow/ArrowSourceOpDescSpec.scala  |  6 ++---
 .../scan/csvOld/CSVOldScanSourceOpDescSpec.scala   |  5 ++--
 .../scan/json/JSONLScanSourceOpDescSpec.scala      |  5 ++--
 .../operator/source/sql/SQLSourceOpDescSpec.scala  |  5 ++--
 .../sql/asterixdb/AsterixDBSourceOpDescSpec.scala  |  5 ++--
 .../source/sql/mysql/MySQLSourceOpDescSpec.scala   |  5 ++--
 .../postgresql/PostgreSQLSourceOpDescSpec.scala    |  5 ++--
 .../error-frame/error-frame.component.ts           | 28 +++++++++++++++++++-
 .../texera/amber/compiler/WorkflowCompiler.scala   | 21 +++++++++------
 .../texera/amber/compiler/model/LogicalPlan.scala  |  6 ++++-
 .../amber/compiler/WorkflowCompilerSpec.scala      |  2 +-
 21 files changed, 183 insertions(+), 58 deletions(-)

diff --git a/amber/src/main/scala/org/apache/texera/workflow/LogicalPlan.scala b/amber/src/main/scala/org/apache/texera/workflow/LogicalPlan.scala
index 974d17f40a..6e759439a0 100644
--- a/amber/src/main/scala/org/apache/texera/workflow/LogicalPlan.scala
+++ b/amber/src/main/scala/org/apache/texera/workflow/LogicalPlan.scala
@@ -99,7 +99,11 @@ case class LogicalPlan(
       case operator @ (scanOp: ScanSourceOpDesc) =>
         Try {
           // Resolve file path for ScanSourceOpDesc
-          val fileName = scanOp.fileName.getOrElse(throw new RuntimeException("no input file name"))
+          val fileName = scanOp.fileName.getOrElse(
+            throw new RuntimeException(
+              "No file selected. Please select a file from the 'File' dropdown in the right panel."
+            )
+          )
           val fileUri = FileResolver.resolve(fileName) // Convert to URI
 
           // Set the URI in the ScanSourceOpDesc
diff --git a/amber/src/test/scala/org/apache/texera/workflow/WorkflowCompilerSpec.scala b/amber/src/test/scala/org/apache/texera/workflow/WorkflowCompilerSpec.scala
index 94a8ffce55..3ee61d3f5c 100644
--- a/amber/src/test/scala/org/apache/texera/workflow/WorkflowCompilerSpec.scala
+++ b/amber/src/test/scala/org/apache/texera/workflow/WorkflowCompilerSpec.scala
@@ -198,7 +198,7 @@ class WorkflowCompilerSpec extends AnyFlatSpec {
   "WorkflowCompiler in strict mode (no errorList)" should
     "throw when a scan source has no fileName set" in {
     // CSVScanSourceOpDesc defaults fileName to None; `resolveScanSourceOpFileName(None)`
-    // hits `scanOp.fileName.getOrElse(throw new RuntimeException("no input file name"))`
+    // hits the "No file selected" RuntimeException thrown from `scanOp.fileName.getOrElse`
     // and surfaces that exception out of `compile` because the compiler passes
     // `None` for the errorList (i.e. fail-fast on the execution path).
     val orphanCsv = new CSVScanSourceOpDesc()
@@ -207,6 +207,6 @@ class WorkflowCompilerSpec extends AnyFlatSpec {
     val ex = intercept[RuntimeException] {
       new WorkflowCompiler(ctx).compile(pojo(List(orphanCsv), List.empty))
     }
-    assert(ex.getMessage == "no input file name")
+    assert(ex.getMessage.contains("No file selected"))
   }
 }
diff --git a/common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/projection/ProjectionOpDesc.scala b/common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/projection/ProjectionOpDesc.scala
index 3af25b9832..fb9258410c 100644
--- a/common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/projection/ProjectionOpDesc.scala
+++ b/common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/projection/ProjectionOpDesc.scala
@@ -56,7 +56,7 @@ class ProjectionOpDesc extends MapOpDesc {
       .withOutputPorts(operatorInfo.outputPorts)
       .withDerivePartition(derivePartition())
       .withPropagateSchema(SchemaPropagationFunc(inputSchemas => {
-        require(attributes.nonEmpty, "Attributes must not be empty")
+        require(attributes.nonEmpty, "Please select at least one attribute to project.")
 
         val inputSchema = inputSchemas.values.head
         val outputSchema = if (!isDrop) {
diff --git a/common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/source/scan/arrow/ArrowSourceOpDesc.scala b/common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/source/scan/arrow/ArrowSourceOpDesc.scala
index ac43dbe65d..ad1d7a3417 100644
--- a/common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/source/scan/arrow/ArrowSourceOpDesc.scala
+++ b/common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/source/scan/arrow/ArrowSourceOpDesc.scala
@@ -71,7 +71,20 @@ class ArrowSourceOpDesc extends ScanSourceOpDesc {
     */
   @Override
   def inferSchema(): Schema = {
-    val file = DocumentFactory.openReadonlyDocument(new URI(fileName.get)).asFile()
+    require(
+      fileResolved(),
+      "No file selected. Please select a valid .arrow file from the 'File' dropdown in the right panel."
+    )
+
+    val uri = new URI(fileName.get)
+    if (uri.getScheme == "file") {
+      require(
+        new java.io.File(uri).isFile,
+        "The selected item is a folder or does not exist. Please select an actual .arrow file from the 'File' dropdown."
+      )
+    }
+    val file = DocumentFactory.openReadonlyDocument(uri).asFile()
+
     val allocator = new RootAllocator()
 
     Using
@@ -81,8 +94,15 @@ class ArrowSourceOpDesc extends ScanSourceOpDesc {
         val arrowSchema: ArrowSchema = reader.getVectorSchemaRoot.getSchema
         ArrowUtils.toTexeraSchema(arrowSchema)
       }
-      .getOrElse {
-        throw new IOException("Failed to infer schema from Arrow file.")
+      .recoverWith {
+        case scala.util.control.NonFatal(e) =>
+          scala.util.Failure(
+            new RuntimeException(
+              "Failed to read the .arrow file. Please ensure it is a valid Arrow file.",
+              e
+            )
+          )
       }
+      .get
   }
 }
diff --git a/common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/source/scan/csv/CSVScanSourceOpDesc.scala b/common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/source/scan/csv/CSVScanSourceOpDesc.scala
index d32bbb31c6..328e3b6c1f 100644
--- a/common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/source/scan/csv/CSVScanSourceOpDesc.scala
+++ b/common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/source/scan/csv/CSVScanSourceOpDesc.scala
@@ -78,15 +78,25 @@ class CSVScanSourceOpDesc extends ScanSourceOpDesc {
   }
 
   override def sourceSchema(): Schema = {
-    if (customDelimiter.isEmpty || !fileResolved()) {
-      return null
+    val delimiterChar = customDelimiter.filter(_.nonEmpty).getOrElse(",").charAt(0)
+    require(
+      fileResolved(),
+      "No file selected. Please select a valid .csv file from the 'File' dropdown in the right panel."
+    )
+
+    val uri = new URI(fileName.get)
+    if (uri.getScheme == "file") {
+      require(
+        new java.io.File(uri).isFile,
+        "The selected item is a folder or does not exist. Please select an actual .csv file from the 'File' dropdown."
+      )
     }
-    val stream = DocumentFactory.openReadonlyDocument(new URI(fileName.get)).asInputStream()
+    val stream = DocumentFactory.openReadonlyDocument(uri).asInputStream()
     val inputReader =
       new InputStreamReader(stream, fileEncoding.getCharset)
 
     val csvFormat = new CsvFormat()
-    csvFormat.setDelimiter(customDelimiter.get.charAt(0))
+    csvFormat.setDelimiter(delimiterChar)
     csvFormat.setLineSeparator("\n")
     val csvSetting = new CsvParserSettings()
     csvSetting.setMaxCharsPerColumn(-1)
diff --git a/common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/source/scan/csv/ParallelCSVScanSourceOpDesc.scala b/common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/source/scan/csv/ParallelCSVScanSourceOpDesc.scala
index 9609b4e239..b3d1071b86 100644
--- a/common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/source/scan/csv/ParallelCSVScanSourceOpDesc.scala
+++ b/common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/source/scan/csv/ParallelCSVScanSourceOpDesc.scala
@@ -79,12 +79,22 @@ class ParallelCSVScanSourceOpDesc extends ScanSourceOpDesc {
   }
 
   override def sourceSchema(): Schema = {
-    if (customDelimiter.isEmpty || !fileResolved()) {
-      return null
+    val delimiterChar = customDelimiter.filter(_.nonEmpty).getOrElse(",").charAt(0)
+    require(
+      fileResolved(),
+      "No file selected. Please select a valid .csv file from the 'File' dropdown in the right panel."
+    )
+
+    val uri = new URI(fileName.get)
+    if (uri.getScheme == "file") {
+      require(
+        new java.io.File(uri).isFile,
+        "The selected item is a folder or does not exist. Please select an actual .csv file from the 'File' dropdown."
+      )
     }
-    val file = DocumentFactory.openReadonlyDocument(new URI(fileName.get)).asFile()
+    val file = DocumentFactory.openReadonlyDocument(uri).asFile()
     implicit object CustomFormat extends DefaultCSVFormat {
-      override val delimiter: Char = customDelimiter.get.charAt(0)
+      override val delimiter: Char = delimiterChar
 
     }
     var reader: CSVReader = CSVReader.open(file)(CustomFormat)
diff --git a/common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/source/scan/csvOld/CSVOldScanSourceOpDesc.scala b/common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/source/scan/csvOld/CSVOldScanSourceOpDesc.scala
index cdf9e655cc..2a18d431ff 100644
--- a/common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/source/scan/csvOld/CSVOldScanSourceOpDesc.scala
+++ b/common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/source/scan/csvOld/CSVOldScanSourceOpDesc.scala
@@ -75,13 +75,22 @@ class CSVOldScanSourceOpDesc extends ScanSourceOpDesc {
   }
 
   override def sourceSchema(): Schema = {
-    if (customDelimiter.isEmpty || !fileResolved()) {
-      return null
+    val delimiterChar = customDelimiter.filter(_.nonEmpty).getOrElse(",").charAt(0)
+    require(
+      fileResolved(),
+      "No file selected. Please select a valid .csv file from the 'File' dropdown in the right panel."
+    )
+
+    val uri = new URI(fileName.get)
+    if (uri.getScheme == "file") {
+      require(
+        new java.io.File(uri).isFile,
+        "The selected item is a folder or does not exist. Please select an actual .csv file from the 'File' dropdown."
+      )
     }
-    // infer schema from the first few lines of the file
-    val file = DocumentFactory.openReadonlyDocument(new URI(fileName.get)).asFile()
+    val file = DocumentFactory.openReadonlyDocument(uri).asFile()
     implicit object CustomFormat extends DefaultCSVFormat {
-      override val delimiter: Char = customDelimiter.get.charAt(0)
+      override val delimiter: Char = delimiterChar
     }
     var reader: CSVReader =
       CSVReader.open(file, fileEncoding.getCharset.name())(CustomFormat)
diff --git a/common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/source/scan/json/JSONLScanSourceOpDesc.scala b/common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/source/scan/json/JSONLScanSourceOpDesc.scala
index 5ca429f511..faccc76f88 100644
--- a/common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/source/scan/json/JSONLScanSourceOpDesc.scala
+++ b/common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/source/scan/json/JSONLScanSourceOpDesc.scala
@@ -68,10 +68,19 @@ class JSONLScanSourceOpDesc extends ScanSourceOpDesc {
   }
 
   override def sourceSchema(): Schema = {
-    if (!fileResolved()) {
-      return null
+    require(
+      fileResolved(),
+      "No file selected. Please select a valid .jsonl file from the 'File' dropdown in the right panel."
+    )
+
+    val uri = new URI(fileName.get)
+    if (uri.getScheme == "file") {
+      require(
+        new java.io.File(uri).isFile,
+        "The selected item is a folder or does not exist. Please select an actual .jsonl file from the 'File' dropdown."
+      )
     }
-    val stream = DocumentFactory.openReadonlyDocument(new URI(fileName.get)).asInputStream()
+    val stream = DocumentFactory.openReadonlyDocument(uri).asInputStream()
     val reader = new BufferedReader(new InputStreamReader(stream, fileEncoding.getCharset))
     var fieldNames = Set[String]()
 
diff --git a/common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/source/sql/SQLSourceOpDesc.scala b/common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/source/sql/SQLSourceOpDesc.scala
index 4cccd0ff2a..5277cd25c8 100644
--- a/common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/source/sql/SQLSourceOpDesc.scala
+++ b/common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/source/sql/SQLSourceOpDesc.scala
@@ -138,12 +138,30 @@ abstract class SQLSourceOpDesc extends SourceOperatorDescriptor {
     * @return Schema
     */
   private def querySchema: Schema = {
-    if (
-      this.host == null || this.port == null || this.database == null
-      || this.table == null || this.username == null || this.password == null
-    ) {
-      return null
-    }
+    require(
+      host != null && host.trim.nonEmpty,
+      s"Please enter a valid host name for the database in the properties panel."
+    )
+    require(
+      port != null && port.trim.nonEmpty,
+      s"Please enter a valid port for the database in the properties panel."
+    )
+    require(
+      database != null && database.trim.nonEmpty,
+      s"Please enter a valid database name in the properties panel."
+    )
+    require(
+      table != null && table.trim.nonEmpty,
+      s"Please enter a valid table name in the properties panel."
+    )
+    require(
+      username != null && username.trim.nonEmpty,
+      s"Please enter a valid username in the properties panel."
+    )
+    require(
+      password != null && password.trim.nonEmpty,
+      s"Please enter a valid password in the properties panel."
+    )
 
     updatePort()
     try {
diff --git a/common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/source/sql/asterixdb/AsterixDBSourceOpDesc.scala b/common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/source/sql/asterixdb/AsterixDBSourceOpDesc.scala
index d12d334e70..0ef06d740d 100644
--- a/common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/source/sql/asterixdb/AsterixDBSourceOpDesc.scala
+++ b/common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/source/sql/asterixdb/AsterixDBSourceOpDesc.scala
@@ -139,9 +139,13 @@ class AsterixDBSourceOpDesc extends SQLSourceOpDesc {
   override def updatePort(): Unit = port = if (port.trim().equals("default")) "19002" else port
 
   override def sourceSchema(): Schema = {
-    if (this.host == null || this.port == null || this.database == null || this.table == null) {
-      return null
-    }
+    require(host != null && host.trim.nonEmpty, "Please enter a valid host name for AsterixDB.")
+    require(port != null && port.trim.nonEmpty, "Please enter a valid port for AsterixDB.")
+    require(
+      database != null && database.trim.nonEmpty,
+      "Please enter a valid database name for AsterixDB."
+    )
+    require(table != null && table.trim.nonEmpty, "Please enter a valid table name for AsterixDB.")
 
     updatePort()
 
diff --git a/common/workflow-operator/src/test/scala/org/apache/texera/amber/operator/source/scan/arrow/ArrowSourceOpDescSpec.scala b/common/workflow-operator/src/test/scala/org/apache/texera/amber/operator/source/scan/arrow/ArrowSourceOpDescSpec.scala
index 6bd54c2c0c..54dd677913 100644
--- a/common/workflow-operator/src/test/scala/org/apache/texera/amber/operator/source/scan/arrow/ArrowSourceOpDescSpec.scala
+++ b/common/workflow-operator/src/test/scala/org/apache/texera/amber/operator/source/scan/arrow/ArrowSourceOpDescSpec.scala
@@ -153,13 +153,13 @@ class ArrowSourceOpDescSpec extends AnyFlatSpec with Matchers {
     d.inferSchema() shouldBe schema
   }
 
-  it should "throw an IOException when the file is not a valid Arrow file" in {
+  it should "throw a friendly error when the file is not a valid Arrow file" in {
     val bogus = File.createTempFile("not-arrow-", ".arrow")
     bogus.deleteOnExit()
     Files.write(bogus.toPath, "this is not arrow".getBytes)
     val d = new ArrowSourceOpDesc
     d.fileName = Some(bogus.toURI.toString)
-    val ex = intercept[java.io.IOException](d.inferSchema())
-    ex.getMessage shouldBe "Failed to infer schema from Arrow file."
+    val ex = intercept[RuntimeException](d.inferSchema())
+    ex.getMessage shouldBe "Failed to read the .arrow file. Please ensure it is a valid Arrow file."
   }
 }
diff --git a/common/workflow-operator/src/test/scala/org/apache/texera/amber/operator/source/scan/csvOld/CSVOldScanSourceOpDescSpec.scala b/common/workflow-operator/src/test/scala/org/apache/texera/amber/operator/source/scan/csvOld/CSVOldScanSourceOpDescSpec.scala
index e0d7efabf0..b72476641b 100644
--- a/common/workflow-operator/src/test/scala/org/apache/texera/amber/operator/source/scan/csvOld/CSVOldScanSourceOpDescSpec.scala
+++ b/common/workflow-operator/src/test/scala/org/apache/texera/amber/operator/source/scan/csvOld/CSVOldScanSourceOpDescSpec.scala
@@ -54,8 +54,9 @@ class CSVOldScanSourceOpDescSpec extends AnyFlatSpec with Matchers {
     d.fileTypeName shouldBe Some("CSVOld")
   }
 
-  "CSVOldScanSourceOpDesc.sourceSchema" should "be null before a file is resolved" in {
-    (new CSVOldScanSourceOpDesc).sourceSchema() shouldBe null
+  "CSVOldScanSourceOpDesc.sourceSchema" should "prompt for a file before one is resolved" in {
+    val ex = intercept[IllegalArgumentException]((new CSVOldScanSourceOpDesc).sourceSchema())
+    ex.getMessage should include("No file selected")
   }
 
   "CSVOldScanSourceOpDesc.getPhysicalOp" should
diff --git a/common/workflow-operator/src/test/scala/org/apache/texera/amber/operator/source/scan/json/JSONLScanSourceOpDescSpec.scala b/common/workflow-operator/src/test/scala/org/apache/texera/amber/operator/source/scan/json/JSONLScanSourceOpDescSpec.scala
index 7f34281b19..c0d8cf2e93 100644
--- a/common/workflow-operator/src/test/scala/org/apache/texera/amber/operator/source/scan/json/JSONLScanSourceOpDescSpec.scala
+++ b/common/workflow-operator/src/test/scala/org/apache/texera/amber/operator/source/scan/json/JSONLScanSourceOpDescSpec.scala
@@ -53,8 +53,9 @@ class JSONLScanSourceOpDescSpec extends AnyFlatSpec with Matchers {
     d.fileTypeName shouldBe Some("JSONL")
   }
 
-  "JSONLScanSourceOpDesc.sourceSchema" should "be null before a file is resolved" in {
-    (new JSONLScanSourceOpDesc).sourceSchema() shouldBe null
+  "JSONLScanSourceOpDesc.sourceSchema" should "prompt for a file before one is resolved" in {
+    val ex = intercept[IllegalArgumentException]((new JSONLScanSourceOpDesc).sourceSchema())
+    ex.getMessage should include("No file selected")
   }
 
   "JSONLScanSourceOpDesc.getPhysicalOp" should
diff --git a/common/workflow-operator/src/test/scala/org/apache/texera/amber/operator/source/sql/SQLSourceOpDescSpec.scala b/common/workflow-operator/src/test/scala/org/apache/texera/amber/operator/source/sql/SQLSourceOpDescSpec.scala
index 5f715bb2da..077f138078 100644
--- a/common/workflow-operator/src/test/scala/org/apache/texera/amber/operator/source/sql/SQLSourceOpDescSpec.scala
+++ b/common/workflow-operator/src/test/scala/org/apache/texera/amber/operator/source/sql/SQLSourceOpDescSpec.scala
@@ -89,10 +89,11 @@ class SQLSourceOpDescSpec extends AnyFlatSpec with Matchers with MockFactory {
     configure(new TestSQLSourceOpDesc(conn)).sourceSchema().getAttribute("c").getType
   }
 
-  "SQLSourceOpDesc.sourceSchema" should "return null until every connection field is set" in {
+  "SQLSourceOpDesc.sourceSchema" should "prompt for the missing connection field" in {
     val desc = configure(new TestSQLSourceOpDesc(mock[Connection]))
     desc.table = null
-    assert(desc.sourceSchema() == null)
+    val ex = intercept[IllegalArgumentException](desc.sourceSchema())
+    ex.getMessage should include("table")
   }
 
   it should "map each JDBC data type to its Texera attribute type" in {
diff --git a/common/workflow-operator/src/test/scala/org/apache/texera/amber/operator/source/sql/asterixdb/AsterixDBSourceOpDescSpec.scala b/common/workflow-operator/src/test/scala/org/apache/texera/amber/operator/source/sql/asterixdb/AsterixDBSourceOpDescSpec.scala
index 84f3e6a6c9..2f8a935247 100644
--- a/common/workflow-operator/src/test/scala/org/apache/texera/amber/operator/source/sql/asterixdb/AsterixDBSourceOpDescSpec.scala
+++ b/common/workflow-operator/src/test/scala/org/apache/texera/amber/operator/source/sql/asterixdb/AsterixDBSourceOpDescSpec.scala
@@ -56,8 +56,9 @@ class AsterixDBSourceOpDescSpec extends AnyFlatSpec with Matchers {
     d.interval shouldBe 0L
   }
 
-  "AsterixDBSourceOpDesc.sourceSchema" should "be null before a connection is configured" in {
-    (new AsterixDBSourceOpDesc).sourceSchema() shouldBe null
+  "AsterixDBSourceOpDesc.sourceSchema" should "prompt for connection details before a connection is configured" in {
+    val ex = intercept[IllegalArgumentException]((new AsterixDBSourceOpDesc).sourceSchema())
+    ex.getMessage should include("host")
   }
 
   "AsterixDBSourceOpDesc.getPhysicalOp" should
diff --git a/common/workflow-operator/src/test/scala/org/apache/texera/amber/operator/source/sql/mysql/MySQLSourceOpDescSpec.scala b/common/workflow-operator/src/test/scala/org/apache/texera/amber/operator/source/sql/mysql/MySQLSourceOpDescSpec.scala
index cde2c63680..618cebc22f 100644
--- a/common/workflow-operator/src/test/scala/org/apache/texera/amber/operator/source/sql/mysql/MySQLSourceOpDescSpec.scala
+++ b/common/workflow-operator/src/test/scala/org/apache/texera/amber/operator/source/sql/mysql/MySQLSourceOpDescSpec.scala
@@ -60,8 +60,9 @@ class MySQLSourceOpDescSpec extends AnyFlatSpec with Matchers {
     d.interval shouldBe 0L
   }
 
-  "MySQLSourceOpDesc.sourceSchema" should "be null before a connection is configured" in {
-    (new MySQLSourceOpDesc).sourceSchema() shouldBe null
+  "MySQLSourceOpDesc.sourceSchema" should "prompt for connection details before a connection is configured" in {
+    val ex = intercept[IllegalArgumentException]((new MySQLSourceOpDesc).sourceSchema())
+    ex.getMessage should include("host")
   }
 
   "MySQLSourceOpDesc.getPhysicalOp" should
diff --git a/common/workflow-operator/src/test/scala/org/apache/texera/amber/operator/source/sql/postgresql/PostgreSQLSourceOpDescSpec.scala b/common/workflow-operator/src/test/scala/org/apache/texera/amber/operator/source/sql/postgresql/PostgreSQLSourceOpDescSpec.scala
index df158479cf..d32f087d7e 100644
--- a/common/workflow-operator/src/test/scala/org/apache/texera/amber/operator/source/sql/postgresql/PostgreSQLSourceOpDescSpec.scala
+++ b/common/workflow-operator/src/test/scala/org/apache/texera/amber/operator/source/sql/postgresql/PostgreSQLSourceOpDescSpec.scala
@@ -55,8 +55,9 @@ class PostgreSQLSourceOpDescSpec extends AnyFlatSpec with Matchers {
     d.interval shouldBe 0L
   }
 
-  "PostgreSQLSourceOpDesc.sourceSchema" should "be null before a connection is configured" in {
-    (new PostgreSQLSourceOpDesc).sourceSchema() shouldBe null
+  "PostgreSQLSourceOpDesc.sourceSchema" should "prompt for connection details before a connection is configured" in {
+    val ex = intercept[IllegalArgumentException]((new PostgreSQLSourceOpDesc).sourceSchema())
+    ex.getMessage should include("host")
   }
 
   "PostgreSQLSourceOpDesc.getPhysicalOp" should
diff --git a/frontend/src/app/workspace/component/result-panel/error-frame/error-frame.component.ts b/frontend/src/app/workspace/component/result-panel/error-frame/error-frame.component.ts
index e90a5288db..41b5cac55a 100644
--- a/frontend/src/app/workspace/component/result-panel/error-frame/error-frame.component.ts
+++ b/frontend/src/app/workspace/component/result-panel/error-frame/error-frame.component.ts
@@ -73,10 +73,36 @@ export class ErrorFrameComponent implements OnInit {
     }
     this.categoryToErrorMapping = errorMessages.reduce((acc, obj) => {
       const key = obj.type.name;
+      let message = obj.message;
+      let details = obj.details;
+
+      const shouldFormat = key === "COMPILATION_ERROR" || key === "EXECUTION_FAILURE";
+      if (shouldFormat) {
+        // Strip out common Java exception class names and formatting to make it more user-friendly
+        const exceptionRegex = /^\s*(?:(?:[a-zA-Z0-9_]+\.)*[a-zA-Z0-9_]+(?:Exception|Error)):\s*/;
+        const requirementFailedRegex = /^\s*requirement failed:\s*/;
+
+        if (message) {
+          message = message.replace(exceptionRegex, "");
+          message = message.replace(requirementFailedRegex, "");
+        }
+
+        if (details) {
+          details = details.replace(exceptionRegex, "");
+          details = details.replace(requirementFailedRegex, "");
+        }
+      }
+
+      const formattedError: WorkflowFatalError = {
+        ...obj,
+        message: message,
+        details: details,
+      };
+
       if (!acc.has(key)) {
         acc.set(key, []);
       }
-      acc.get(key)!.push(obj);
+      acc.get(key)!.push(formattedError);
       return acc;
     }, new Map<string, WorkflowFatalError[]>());
   }
diff --git a/workflow-compiling-service/src/main/scala/org/apache/texera/amber/compiler/WorkflowCompiler.scala b/workflow-compiling-service/src/main/scala/org/apache/texera/amber/compiler/WorkflowCompiler.scala
index 25166e7ac5..666ddb70a8 100644
--- a/workflow-compiling-service/src/main/scala/org/apache/texera/amber/compiler/WorkflowCompiler.scala
+++ b/workflow-compiling-service/src/main/scala/org/apache/texera/amber/compiler/WorkflowCompiler.scala
@@ -64,17 +64,22 @@ object WorkflowCompiler {
       errorList: List[(OperatorIdentity, Throwable)]
   ): Map[OperatorIdentity, WorkflowFatalError] = {
     val opIdToError = mutable.Map[OperatorIdentity, WorkflowFatalError]()
-    errorList.map {
+    errorList.foreach {
       case (opId, err) =>
         // map each error to WorkflowFatalError, and report them in the log
         logger.error(s"Error occurred in logical plan compilation for opId: $opId", err)
-        opIdToError += (opId -> WorkflowFatalError(
-          COMPILATION_ERROR,
-          Timestamp(Instant.now),
-          err.toString,
-          getStackTraceWithAllCauses(err),
-          opId.id
-        ))
+        // keep only the first error per operator: it is the root cause (e.g. a file
+        // resolution failure), while later stages re-fail on the same operator with
+        // less specific messages (e.g. schema propagation seeing an unresolved file)
+        if (!opIdToError.contains(opId)) {
+          opIdToError += (opId -> WorkflowFatalError(
+            COMPILATION_ERROR,
+            Timestamp(Instant.now),
+            err.toString,
+            getStackTraceWithAllCauses(err),
+            opId.id
+          ))
+        }
     }
     opIdToError.toMap
   }
diff --git a/workflow-compiling-service/src/main/scala/org/apache/texera/amber/compiler/model/LogicalPlan.scala b/workflow-compiling-service/src/main/scala/org/apache/texera/amber/compiler/model/LogicalPlan.scala
index eecb435cc8..d7fb25679b 100644
--- a/workflow-compiling-service/src/main/scala/org/apache/texera/amber/compiler/model/LogicalPlan.scala
+++ b/workflow-compiling-service/src/main/scala/org/apache/texera/amber/compiler/model/LogicalPlan.scala
@@ -117,7 +117,11 @@ case class LogicalPlan(
       case operator @ (scanOp: ScanSourceOpDesc) =>
         Try {
           // Resolve file path for ScanSourceOpDesc
-          val fileName = scanOp.fileName.getOrElse(throw new RuntimeException("no input file name"))
+          val fileName = scanOp.fileName.getOrElse(
+            throw new RuntimeException(
+              "No file selected. Please select a file from the 'File' dropdown in the right panel."
+            )
+          )
           val fileUri = FileResolver.resolve(fileName) // Convert to URI
 
           // Set the URI in the ScanSourceOpDesc
diff --git a/workflow-compiling-service/src/test/scala/org/apache/texera/amber/compiler/WorkflowCompilerSpec.scala b/workflow-compiling-service/src/test/scala/org/apache/texera/amber/compiler/WorkflowCompilerSpec.scala
index ee221728d3..79df9e98ee 100644
--- a/workflow-compiling-service/src/test/scala/org/apache/texera/amber/compiler/WorkflowCompilerSpec.scala
+++ b/workflow-compiling-service/src/test/scala/org/apache/texera/amber/compiler/WorkflowCompilerSpec.scala
@@ -210,7 +210,7 @@ class WorkflowCompilerSpec extends AnyFlatSpec {
     val err = result.operatorIdToError(orphan.operatorIdentifier)
     assert(err.`type` == COMPILATION_ERROR)
     assert(err.operatorId == orphan.operatorIdentifier.id)
-    assert(err.message.contains("no input file name"), s"unexpected message: ${err.message}")
+    assert(err.message.contains("No file selected"), s"unexpected message: ${err.message}")
     assert(err.details.nonEmpty, "stack-trace details should be populated for UI display")
   }