KCL: Emit proper errors in unexpected edge cases (#7351)

There's some bug in the frontend or KCL somewhere, which results in the TypeScript frontend sending an AST (serialized to JSON) to the KCL executor, but the JSON cannot be deserialized into an AST. If this happens, it's a bug in ZDS, not a user error. 

The problem is that this sort of error will cause the frontend to silently stop rendering KCL, and it won't show the user any errors. They need to open up the console and look at the error there, and even if they do, it's hard to understand.

This PR changes how we report these unexpected errors due to bugs in ZDS. ZDS should not silently stop working, it should at least print a half-decent error like this:

<img width="527" alt="nicer error" src="https://github.com/user-attachments/assets/1bb37a64-0915-4472-849c-d146f397356b" />

## Fix

Right now, the wasm library exports a function `execute`. It previous returned an error as a String if one occurred. The frontend assumed this error string would be JSON that matched the schema `KclErrorWithOutputs`. This was not always true! For example, if something couldn't be serialized to JSON, we'd take the raw Serde error and stringify that. It wouldn't match `KclErrorWithOutputs`.

Now I've changed `execute` so that if it errors, it'll returns a JsValue not a string. So that's one check (can this string be deserialized into a JSON object) that can be removed -- it'll return a JSON object directly now. The next check is "does this JSON object conform to the KclErrorWithOutputs schema". To prove that's correct, I changed `execute` to be a thin wrapper around `fn execute_typed` which returns `Result<ExecOutcome, KclErrorWithOutputs>`. Now we know the error will be the right type.
This commit is contained in:
Adam Chalmers
2025-06-03 15:37:17 -05:00
committed by GitHub
parent 2af2144f89
commit b47b9c9613
3 changed files with 45 additions and 13 deletions

View File

@ -1189,7 +1189,6 @@ impl LanguageServer for Backend {
}
async fn completion(&self, params: CompletionParams) -> RpcResult<Option<CompletionResponse>> {
// ADAM: This is the entrypoint.
let mut completions = vec![CompletionItem {
label: PIPE_OPERATOR.to_string(),
label_details: None,

View File

@ -3,7 +3,7 @@
use std::sync::Arc;
use gloo_utils::format::JsValueSerdeExt;
use kcl_lib::{wasm_engine::FileManager, EngineManager, Program};
use kcl_lib::{wasm_engine::FileManager, EngineManager, ExecOutcome, KclError, KclErrorWithOutputs, Program};
use wasm_bindgen::prelude::*;
#[wasm_bindgen]
@ -56,7 +56,7 @@ impl Context {
return Ok(kcl_lib::ExecutorContext::new_mock(
self.mock_engine.clone(),
self.fs.clone(),
settings.into(),
settings,
));
}
@ -74,18 +74,38 @@ impl Context {
program_ast_json: &str,
path: Option<String>,
settings: &str,
) -> Result<JsValue, String> {
) -> Result<JsValue, JsValue> {
console_error_panic_hook::set_once();
let program: Program = serde_json::from_str(program_ast_json).map_err(|e| e.to_string())?;
self.execute_typed(program_ast_json, path, settings)
.await
.and_then(|outcome|JsValue::from_serde(&outcome).map_err(|e| {
// The serde-wasm-bindgen does not work here because of weird HashMap issues.
// DO NOT USE serde_wasm_bindgen::to_value it will break the frontend.
KclErrorWithOutputs::no_outputs(KclError::internal(
format!("Could not serialize successful KCL result. This is a bug in KCL and not in your code, please report this to Zoo. Details: {e}"),
))}))
.map_err(|e: KclErrorWithOutputs| JsValue::from_serde(&e).unwrap())
}
let ctx = self.create_executor_ctx(settings, path, false)?;
match ctx.run_with_caching(program).await {
// The serde-wasm-bindgen does not work here because of weird HashMap issues.
// DO NOT USE serde_wasm_bindgen::to_value it will break the frontend.
Ok(outcome) => JsValue::from_serde(&outcome).map_err(|e| e.to_string()),
Err(err) => Err(serde_json::to_string(&err).map_err(|serde_err| serde_err.to_string())?),
}
async fn execute_typed(
&self,
program_ast_json: &str,
path: Option<String>,
settings: &str,
) -> Result<ExecOutcome, KclErrorWithOutputs> {
let program: Program = serde_json::from_str(program_ast_json).map_err(|e| {
let err = KclError::internal(
format!("Could not deserialize KCL AST. This is a bug in KCL and not in your code, please report this to Zoo. Details: {e}"),
);
KclErrorWithOutputs::no_outputs(err)
})?;
let ctx = self
.create_executor_ctx(settings, path, false)
.map_err(|e| KclErrorWithOutputs::no_outputs(KclError::internal(
format!("Could not create KCL executor context. This is a bug in KCL and not in your code, please report this to Zoo. Details: {e}"),
)))?;
ctx.run_with_caching(program).await
}
/// Reset the scene and bust the cache.

View File

@ -389,7 +389,20 @@ export function sketchFromKclValue(
}
export const errFromErrWithOutputs = (e: any): KCLError => {
const parsed: KclErrorWithOutputs = JSON.parse(e.toString())
// `e` is any, so let's figure out something useful to do with it.
const parsed: KclErrorWithOutputs = (() => {
// No need to parse, it's already an object.
if (typeof e === 'object') {
return e
}
// It's a string, so parse it.
if (typeof e === 'string') {
return JSON.parse(e)
}
// It can be converted to a string, then parsed.
return JSON.parse(e.toString())
})()
return new KCLError(
parsed.error.kind,
parsed.error.details.msg,