From b1ee1aab73f5bb2cc31e66cf76df4bc3cf376bdb Mon Sep 17 00:00:00 2001 From: Sergio Date: Sun, 8 Mar 2026 22:48:02 -0700 Subject: [PATCH 1/7] cli: return non-zero on uncaught runtime exceptions --- cli/src/main.rs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/cli/src/main.rs b/cli/src/main.rs index abb0418a764..b29ac3c86ae 100644 --- a/cli/src/main.rs +++ b/cli/src/main.rs @@ -503,7 +503,10 @@ fn evaluate_file( println!("{}", v.display()); } } - Err(v) => printer.print(uncaught_error(&v)), + Err(v) => { + printer.print(uncaught_error(&v)); + return Err(v.into_erased(context).into()); + } } Ok(()) From 8de529d68ccfc18438d891c23d2973ef61e013ce Mon Sep 17 00:00:00 2001 From: sergiochan Date: Tue, 10 Mar 2026 22:28:49 -0700 Subject: [PATCH 2/7] cli: return non-zero for stdin/expression uncaught errors --- cli/src/main.rs | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/cli/src/main.rs b/cli/src/main.rs index b29ac3c86ae..cd63d470864 100644 --- a/cli/src/main.rs +++ b/cli/src/main.rs @@ -411,15 +411,22 @@ fn evaluate_expr( let result = script.evaluate(context); if let Err(err) = context.run_jobs() { printer.print(uncaught_job_error(&err)); + return Err(err.into_erased(context).into()); } 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(v.into_erased(context).into()); + } } } - Err(ref v) => printer.print(uncaught_error(v)), + Err(v) => { + printer.print(uncaught_error(&v)); + return Err(v.into_erased(context).into()); + } } } From 984e48e8ad6ae503dc07d5cc883d17541bfb938f Mon Sep 17 00:00:00 2001 From: sergiochan Date: Tue, 10 Mar 2026 22:45:09 -0700 Subject: [PATCH 3/7] cli: add regression tests for uncaught-error exit code --- cli/Cargo.toml | 3 +++ cli/tests/exit_code.rs | 19 +++++++++++++++++++ 2 files changed, 22 insertions(+) create mode 100644 cli/tests/exit_code.rs diff --git a/cli/Cargo.toml b/cli/Cargo.toml index 80a153a00bb..488436ea503 100644 --- a/cli/Cargo.toml +++ b/cli/Cargo.toml @@ -30,6 +30,9 @@ futures-lite.workspace = true async-channel.workspace = true rustls.workspace = true +[dev-dependencies] +assert_cmd = "2.0.16" + [features] default = [ "boa_engine/annex-b", diff --git a/cli/tests/exit_code.rs b/cli/tests/exit_code.rs new file mode 100644 index 00000000000..92d08151bb5 --- /dev/null +++ b/cli/tests/exit_code.rs @@ -0,0 +1,19 @@ +use assert_cmd::Command; + +#[test] +fn stdin_uncaught_error_exits_non_zero() { + Command::cargo_bin("boa") + .expect("boa binary should build") + .write_stdin("throw Error('nooo')") + .assert() + .failure(); +} + +#[test] +fn expression_uncaught_error_exits_non_zero() { + Command::cargo_bin("boa") + .expect("boa binary should build") + .args(["-e", "throw Error('nooo')"]) + .assert() + .failure(); +} From 2d1c0d552b7f0696998fb72c81cd8d4f81605e8a Mon Sep 17 00:00:00 2001 From: sergiochan Date: Wed, 11 Mar 2026 20:30:35 -0700 Subject: [PATCH 4/7] test(cli): fix strict linting for exit-code regression tests --- cli/src/main.rs | 3 +++ cli/tests/exit_code.rs | 2 ++ 2 files changed, 5 insertions(+) diff --git a/cli/src/main.rs b/cli/src/main.rs index cd63d470864..0c67c6a1954 100644 --- a/cli/src/main.rs +++ b/cli/src/main.rs @@ -12,6 +12,9 @@ mod executor; mod helper; mod logger; +#[cfg(test)] +use assert_cmd as _; + use crate::executor::Executor; use crate::logger::SharedExternalPrinterLogger; use async_channel::Sender; diff --git a/cli/tests/exit_code.rs b/cli/tests/exit_code.rs index 92d08151bb5..0450987dce9 100644 --- a/cli/tests/exit_code.rs +++ b/cli/tests/exit_code.rs @@ -1,3 +1,5 @@ +#![allow(missing_docs, unused_crate_dependencies)] + use assert_cmd::Command; #[test] From 844d840f17099a14de2b469588caef1938ed1ad1 Mon Sep 17 00:00:00 2001 From: sergiochan Date: Fri, 13 Mar 2026 01:33:41 -0700 Subject: [PATCH 5/7] test(cli): remove assert_cmd dependency from exit-code tests --- cli/Cargo.toml | 3 --- cli/src/main.rs | 3 --- cli/tests/exit_code.rs | 41 ++++++++++++++++++++++++++++++----------- 3 files changed, 30 insertions(+), 17 deletions(-) diff --git a/cli/Cargo.toml b/cli/Cargo.toml index 488436ea503..80a153a00bb 100644 --- a/cli/Cargo.toml +++ b/cli/Cargo.toml @@ -30,9 +30,6 @@ futures-lite.workspace = true async-channel.workspace = true rustls.workspace = true -[dev-dependencies] -assert_cmd = "2.0.16" - [features] default = [ "boa_engine/annex-b", diff --git a/cli/src/main.rs b/cli/src/main.rs index 0c67c6a1954..cd63d470864 100644 --- a/cli/src/main.rs +++ b/cli/src/main.rs @@ -12,9 +12,6 @@ mod executor; mod helper; mod logger; -#[cfg(test)] -use assert_cmd as _; - use crate::executor::Executor; use crate::logger::SharedExternalPrinterLogger; use async_channel::Sender; diff --git a/cli/tests/exit_code.rs b/cli/tests/exit_code.rs index 0450987dce9..3ce1ccda9f3 100644 --- a/cli/tests/exit_code.rs +++ b/cli/tests/exit_code.rs @@ -1,21 +1,40 @@ -#![allow(missing_docs, unused_crate_dependencies)] +#![allow(missing_docs)] -use assert_cmd::Command; +use std::io::Write; +use std::process::{Command, Stdio}; + +fn boa_bin() -> &'static str { + env!("CARGO_BIN_EXE_boa") +} #[test] fn stdin_uncaught_error_exits_non_zero() { - Command::cargo_bin("boa") - .expect("boa binary should build") - .write_stdin("throw Error('nooo')") - .assert() - .failure(); + 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() { - Command::cargo_bin("boa") - .expect("boa binary should build") + let status = Command::new(boa_bin()) .args(["-e", "throw Error('nooo')"]) - .assert() - .failure(); + .stdout(Stdio::null()) + .stderr(Stdio::null()) + .status() + .expect("boa should run"); + + assert!(!status.success(), "expected non-zero exit for uncaught -e error"); } From 10edb5ca536988cddfffd0e8187eb06f958cfcf2 Mon Sep 17 00:00:00 2001 From: sergiochan Date: Thu, 19 Mar 2026 09:11:07 -0700 Subject: [PATCH 6/7] cli: avoid duplicate stderr details for uncaught -e/stdin errors --- cli/src/main.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/cli/src/main.rs b/cli/src/main.rs index cd63d470864..63c174bc522 100644 --- a/cli/src/main.rs +++ b/cli/src/main.rs @@ -411,7 +411,7 @@ fn evaluate_expr( let result = script.evaluate(context); if let Err(err) = context.run_jobs() { printer.print(uncaught_job_error(&err)); - return Err(err.into_erased(context).into()); + return Err(eyre!("execution failed")); } result }; @@ -419,13 +419,13 @@ fn evaluate_expr( Ok(v) => printer.print(format!("{}\n", v.display())), Err(v) => { printer.print(uncaught_error(&v)); - return Err(v.into_erased(context).into()); + return Err(eyre!("execution failed")); } } } Err(v) => { printer.print(uncaught_error(&v)); - return Err(v.into_erased(context).into()); + return Err(eyre!("parsing failed")); } } } From be429baf92867fdd086e5a949911b640e13be63f Mon Sep 17 00:00:00 2001 From: sergiochan Date: Fri, 20 Mar 2026 20:16:18 -0700 Subject: [PATCH 7/7] cli: avoid duplicate stderr details for file uncaught errors --- cli/src/main.rs | 2 +- cli/tests/exit_code.rs | 26 ++++++++++++++++++++++++++ 2 files changed, 27 insertions(+), 1 deletion(-) diff --git a/cli/src/main.rs b/cli/src/main.rs index 63c174bc522..ffe95be38e4 100644 --- a/cli/src/main.rs +++ b/cli/src/main.rs @@ -512,7 +512,7 @@ fn evaluate_file( } Err(v) => { printer.print(uncaught_error(&v)); - return Err(v.into_erased(context).into()); + return Err(eyre!("execution failed")); } } diff --git a/cli/tests/exit_code.rs b/cli/tests/exit_code.rs index 3ce1ccda9f3..f853f600e92 100644 --- a/cli/tests/exit_code.rs +++ b/cli/tests/exit_code.rs @@ -1,7 +1,9 @@ #![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") @@ -38,3 +40,27 @@ fn expression_uncaught_error_exits_non_zero() { 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" + ); +}