diff --git a/crates/engine/src/cmd.rs b/crates/engine/src/cmd.rs
--- a/crates/engine/src/cmd.rs
+++ b/crates/engine/src/cmd.rs
@@ -33,6 +33,7 @@ pub mod plotters;
 mod props;
 pub use draw2::breakline::BreaklineOptions;
 pub use props::MatchProps;
+pub(crate) use props::UNIT_RANGES;
 pub(crate) use props::{current_color_value, set_current_color_value};
 mod qselect;
 mod selecttools;
diff --git a/crates/engine/src/cmd/props.rs b/crates/engine/src/cmd/props.rs
--- a/crates/engine/src/cmd/props.rs
+++ b/crates/engine/src/cmd/props.rs
@@ -608,9 +608,12 @@ impl Interactive for ThicknessM {
     }
 }
 
+/// The unit variables (lower-case name, lowest, highest value) shared by UNITS and SETVAR.
+pub(crate) const UNIT_RANGES: [(&str, i64, i64); 5] = [("lunits", 1, 5), ("luprec", 0, 8), ("aunits", 0, 4), ("auprec", 0, 8), ("insunits", 0, 24)];
+
 fn run_units(s: &mut Session, p: &Value) -> Result<Value> {
     let d = s.doc_mut()?;
-    for (k, lo, hi) in [("lunits", 1, 5), ("luprec", 0, 8), ("aunits", 0, 4), ("auprec", 0, 8), ("insunits", 0, 24)] {
+    for (k, lo, hi) in UNIT_RANGES {
         if let Some(v) = p.get(k).and_then(Value::as_i64) {
             d.header.set_i64(&k.to_ascii_uppercase(), v.clamp(lo, hi));
         }
diff --git a/crates/engine/src/sysvars.rs b/crates/engine/src/sysvars.rs
--- a/crates/engine/src/sysvars.rs
+++ b/crates/engine/src/sysvars.rs
@@ -375,6 +375,15 @@ pub fn set(s: &mut Session, name: &str, v: &Value) -> Result<()> {
             let a = a.filter(|a| a.is_finite()).ok_or_else(bad)?;
             s.doc_mut()?.header.set_f64("ANGBASE", cadcraft_geom::norm_angle(a));
         }
+        // Whole numbers inside the range UNITS clamps to.
+        "LUNITS" | "LUPREC" | "AUNITS" | "AUPREC" => {
+            let (_, lo, hi) = crate::cmd::UNIT_RANGES.iter().find(|(k, ..)| k.eq_ignore_ascii_case(&n)).copied().ok_or_else(bad)?;
+            let i = as_i(v)
+                .or_else(|| as_f64(v).filter(|f| f.fract() == 0.0 && f.abs() < 1e15).map(|f| f as i64))
+                .filter(|i| (lo..=hi).contains(i))
+                .ok_or_else(bad)?;
+            s.doc_mut()?.header.set_i64(&n, i);
+        }
         "CONSTRAINTINFER" => s.doc_mut()?.parametric.settings.infer = as_bool(v).ok_or_else(bad)?,
         _ => {
             if let Some(r) = table::set(s, &n, v) {
diff --git a/crates/engine/src/tests.rs b/crates/engine/src/tests.rs
--- a/crates/engine/src/tests.rs
+++ b/crates/engine/src/tests.rs
@@ -633,6 +633,17 @@ fn setvar_keeps_header_types() {
     assert_eq!(s.doc().unwrap().header.f64("LTSCALE", 0.0), 1.0);
 }
 
+#[test]
+fn setvar_rejects_out_of_range_units() {
+    let mut s = Session::new();
+    for (name, v) in [("LUPREC", 100), ("LUNITS", 9), ("LUNITS", 0), ("AUPREC", -5), ("AUNITS", 5)] {
+        assert!(s.execute("setvar", &json!({"name": name, "value": v})).is_err(), "{name} {v}");
+    }
+    s.execute("setvar", &json!({"name": "LUPREC", "value": 8})).unwrap();
+    assert_eq!(s.doc().unwrap().header.i64("LUPREC", 0), 8);
+    assert_ne!(s.doc().unwrap().header.i64("LUNITS", 2), 9);
+}
+
 #[test]
 fn dynamic_input_pointer_settings() {
     let mut s = Session::new();
