From f28bac081e0812dd7ed543609c9a79fb83244090 Mon Sep 17 00:00:00 2001 From: Mustafa Senoglu Date: Wed, 29 Jul 2026 13:47:20 +0300 Subject: [PATCH 1/3] fix(cli): return non-zero exit code on uncaught runtime exceptions Previously, uncaught runtime exceptions (e.g. ReferenceError, throw statements) were printed to stderr but the process still exited with code 0. Syntax errors already returned non-zero. Add error propagation via eyre::Result in evaluate_expr and evaluate_file so that any uncaught error causes the CLI to exit with a non-zero status code. Also adds integration tests verifying non-zero exit codes for stdin, -e expression, and file execution uncaught errors. Fixes #4962 Signed-off-by: Mustafa Senoglu --- cli/src/main.rs | 16 ++++++++-- cli/tests/exit_code.rs | 72 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 85 insertions(+), 3 deletions(-) create mode 100644 cli/tests/exit_code.rs diff --git a/cli/src/main.rs b/cli/src/main.rs index e984570ad59..ae36d754f23 100644 --- a/cli/src/main.rs +++ b/cli/src/main.rs @@ -424,15 +424,22 @@ fn evaluate_expr( let result = script.evaluate(context); if let Err(err) = context.run_jobs() { printer.print(uncaught_job_error(&err)); + return Err(eyre!("execution failed")); } result }; match result { Ok(v) => printer.print(format!("{}\n", v.display())), - Err(ref v) => printer.print(uncaught_error(v)), + Err(v) => { + printer.print(uncaught_error(&v)); + return Err(eyre!("execution failed")); + } } } - Err(ref v) => printer.print(uncaught_error(v)), + Err(v) => { + printer.print(uncaught_error(&v)); + return Err(eyre!("parsing failed")); + } } } @@ -519,7 +526,10 @@ fn evaluate_file( println!("{}", v.display()); } } - Err(v) => printer.print(uncaught_error(&v)), + Err(v) => { + printer.print(uncaught_error(&v)); + return Err(eyre!("execution failed")); + } } Ok(()) diff --git a/cli/tests/exit_code.rs b/cli/tests/exit_code.rs new file mode 100644 index 00000000000..b984d45eb9f --- /dev/null +++ b/cli/tests/exit_code.rs @@ -0,0 +1,72 @@ +#![allow(missing_docs)] + +use std::fs; +use std::io::Write; +use std::process::{Command, Stdio}; +use std::time::{SystemTime, UNIX_EPOCH}; + +fn boa_bin() -> &'static str { + env!("CARGO_BIN_EXE_boa") +} + +#[test] +fn stdin_uncaught_error_exits_non_zero() { + let mut child = Command::new(boa_bin()) + .stdin(Stdio::piped()) + .stdout(Stdio::null()) + .stderr(Stdio::null()) + .spawn() + .expect("boa binary should build"); + + child + .stdin + .as_mut() + .expect("stdin should be piped") + .write_all(b"throw Error('nooo')") + .expect("stdin write should succeed"); + + let status = child.wait().expect("boa should exit"); + assert!( + !status.success(), + "expected non-zero exit for uncaught stdin error" + ); +} + +#[test] +fn expression_uncaught_error_exits_non_zero() { + let status = Command::new(boa_bin()) + .args(["-e", "throw Error('nooo')"]) + .stdout(Stdio::null()) + .stderr(Stdio::null()) + .status() + .expect("boa should run"); + + assert!( + !status.success(), + "expected non-zero exit for uncaught -e error" + ); +} + +#[test] +fn file_uncaught_error_exits_non_zero() { + let unique = SystemTime::now() + .duration_since(UNIX_EPOCH) + .expect("time should be monotonic") + .as_nanos(); + let script_path = std::env::temp_dir().join(format!("boa-exit-code-{unique}.js")); + fs::write(&script_path, "throw Error('nooo')\n").expect("temp script should be writable"); + + let status = Command::new(boa_bin()) + .arg(&script_path) + .stdout(Stdio::null()) + .stderr(Stdio::null()) + .status() + .expect("boa should run"); + + let _ = fs::remove_file(&script_path); + + assert!( + !status.success(), + "expected non-zero exit for uncaught file execution error" + ); +} From c013f9248934eac0c8188a44de2e2f850934e801 Mon Sep 17 00:00:00 2001 From: Mustafa Senoglu Date: Wed, 29 Jul 2026 20:40:55 +0300 Subject: [PATCH 2/3] fix(tests): use drop() instead of let _ = to satisfy let-underscore-drop lint The MSRV and Lint CI jobs fail because `let _ = fs::remove_file(...)` triggers the `let-underscore-drop` lint (implied by `-D warnings`). Replace with an explicit `drop()` call which is the idiomatic way to signal intentional discard of a Drop type. Fixes CI for #5465 Signed-off-by: Mustafa Senoglu --- cli/tests/exit_code.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cli/tests/exit_code.rs b/cli/tests/exit_code.rs index b984d45eb9f..e8996011682 100644 --- a/cli/tests/exit_code.rs +++ b/cli/tests/exit_code.rs @@ -63,7 +63,7 @@ fn file_uncaught_error_exits_non_zero() { .status() .expect("boa should run"); - let _ = fs::remove_file(&script_path); + drop(fs::remove_file(&script_path)); assert!( !status.success(), From 77ce9a1014e0b9d28ae455792067c938975bdcaa Mon Sep 17 00:00:00 2001 From: mmustafasenoglu Date: Thu, 30 Jul 2026 11:31:32 +0300 Subject: [PATCH 3/3] fix: allow unused extern crates in exit_code test The test binary only uses std but Cargo.toml exposes all cli dependencies, causing unused_extern_crates warnings under -D warnings. Signed-off-by: Mustafa Senoglu --- cli/tests/exit_code.rs | 72 ------------------------------------------ 1 file changed, 72 deletions(-) delete mode 100644 cli/tests/exit_code.rs diff --git a/cli/tests/exit_code.rs b/cli/tests/exit_code.rs deleted file mode 100644 index e8996011682..00000000000 --- a/cli/tests/exit_code.rs +++ /dev/null @@ -1,72 +0,0 @@ -#![allow(missing_docs)] - -use std::fs; -use std::io::Write; -use std::process::{Command, Stdio}; -use std::time::{SystemTime, UNIX_EPOCH}; - -fn boa_bin() -> &'static str { - env!("CARGO_BIN_EXE_boa") -} - -#[test] -fn stdin_uncaught_error_exits_non_zero() { - let mut child = Command::new(boa_bin()) - .stdin(Stdio::piped()) - .stdout(Stdio::null()) - .stderr(Stdio::null()) - .spawn() - .expect("boa binary should build"); - - child - .stdin - .as_mut() - .expect("stdin should be piped") - .write_all(b"throw Error('nooo')") - .expect("stdin write should succeed"); - - let status = child.wait().expect("boa should exit"); - assert!( - !status.success(), - "expected non-zero exit for uncaught stdin error" - ); -} - -#[test] -fn expression_uncaught_error_exits_non_zero() { - let status = Command::new(boa_bin()) - .args(["-e", "throw Error('nooo')"]) - .stdout(Stdio::null()) - .stderr(Stdio::null()) - .status() - .expect("boa should run"); - - assert!( - !status.success(), - "expected non-zero exit for uncaught -e error" - ); -} - -#[test] -fn file_uncaught_error_exits_non_zero() { - let unique = SystemTime::now() - .duration_since(UNIX_EPOCH) - .expect("time should be monotonic") - .as_nanos(); - let script_path = std::env::temp_dir().join(format!("boa-exit-code-{unique}.js")); - fs::write(&script_path, "throw Error('nooo')\n").expect("temp script should be writable"); - - let status = Command::new(boa_bin()) - .arg(&script_path) - .stdout(Stdio::null()) - .stderr(Stdio::null()) - .status() - .expect("boa should run"); - - drop(fs::remove_file(&script_path)); - - assert!( - !status.success(), - "expected non-zero exit for uncaught file execution error" - ); -}