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

tisonkun pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/datasketches-rust.git


The following commit(s) were added to refs/heads/main by this push:
     new 47f012d  refactor: make sketch builders fallible (#236)
47f012d is described below

commit 47f012d59506fea734dac1995784aca75b4f4a60
Author: tison <[email protected]>
AuthorDate: Fri Aug 28 15:55:22 2026 +0800

    refactor: make sketch builders fallible (#236)
    
    Signed-off-by: tison <[email protected]>
---
 CHANGELOG.md                                       |   1 +
 datasketches/src/bloom/mod.rs                      |  18 ++-
 datasketches/src/bloom/sketch.rs                   | 155 +++++++++++++--------
 datasketches/src/req/sketch.rs                     |  10 +-
 datasketches/src/tdigest/sketch.rs                 |   3 +-
 datasketches/src/thetafamily/common/hash_table.rs  |  56 +++++++-
 datasketches/src/thetafamily/common/union.rs       |  16 +++
 datasketches/src/thetafamily/theta/a_not_b.rs      |   4 +-
 .../src/thetafamily/theta/jaccard_similarity.rs    |   4 +-
 datasketches/src/thetafamily/theta/mod.rs          |   2 +-
 datasketches/src/thetafamily/theta/sketch.rs       |  73 ++++------
 datasketches/src/thetafamily/theta/union.rs        |  44 +++---
 datasketches/src/thetafamily/tuple/a_not_b.rs      |   4 +-
 datasketches/src/thetafamily/tuple/intersection.rs |   4 +-
 .../src/thetafamily/tuple/jaccard_similarity.rs    |   4 +-
 datasketches/src/thetafamily/tuple/mod.rs          |   2 +-
 datasketches/src/thetafamily/tuple/sketch.rs       |  49 +++----
 datasketches/src/thetafamily/tuple/union.rs        |  46 +++---
 tests-integration/tests/bloom_test/sketch.rs       |  47 ++++---
 tests-integration/tests/serde_tests/bloom.rs       |   8 +-
 tests-integration/tests/serde_tests/theta.rs       |   6 +-
 tests-integration/tests/serde_tests/tuple.rs       |  12 +-
 tests-integration/tests/theta_test/a_not_b.rs      |  16 +--
 tests-integration/tests/theta_test/intersection.rs |  27 ++--
 .../tests/theta_test/jaccard_similarity.rs         |  25 ++--
 tests-integration/tests/theta_test/sketch.rs       |  51 ++++---
 tests-integration/tests/theta_test/union.rs        | 106 ++++++++------
 tests-integration/tests/tuple_test/a_not_b.rs      |  26 ++--
 tests-integration/tests/tuple_test/intersection.rs |  18 +--
 .../tests/tuple_test/jaccard_similarity.rs         |  26 ++--
 tests-integration/tests/tuple_test/main.rs         |   2 +-
 tests-integration/tests/tuple_test/sketch.rs       |  42 ++++--
 tests-integration/tests/tuple_test/union.rs        |  42 ++++--
 33 files changed, 565 insertions(+), 384 deletions(-)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index 6b4ffd6..74a5080 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -7,6 +7,7 @@ All significant changes to this project will be documented in 
this file.
 ### Breaking changes
 
 * Change `ThetaIntersection::to_sketch` and `TupleIntersection::to_sketch` to 
return `Option`. Callers must handle `None` before the first successful update; 
after that, the methods return `Some` even when the intersection is empty.
+* Change `BloomFilterBuilder`, `ThetaSketchBuilder`, `ThetaUnionBuilder`, 
`TupleSketchBuilder`, and `TupleUnionBuilder` to validate their configuration 
in `build`, which now returns `Result`. Callers must handle construction errors 
instead of relying on builder setters or constructors to panic.
 
 ### New features
 
diff --git a/datasketches/src/bloom/mod.rs b/datasketches/src/bloom/mod.rs
index 4ffa0f8..92e5ed8 100644
--- a/datasketches/src/bloom/mod.rs
+++ b/datasketches/src/bloom/mod.rs
@@ -38,7 +38,9 @@
 //! use datasketches::bloom::BloomFilterBuilder;
 //!
 //! // Create a filter optimized for 1000 items with 1% false positive rate
-//! let mut filter = BloomFilterBuilder::with_accuracy(1000, 0.01).build();
+//! let mut filter = BloomFilterBuilder::with_accuracy(1000, 0.01)
+//!     .build()
+//!     .unwrap();
 //!
 //! // Insert items
 //! filter.insert("apple");
@@ -71,7 +73,8 @@
 //!     0.01,   // Target false positive probability (1%)
 //! )
 //! .seed(9001) // Optional: custom seed
-//! .build();
+//! .build()
+//! .unwrap();
 //! ```
 //!
 //! ## By Size (Manual)
@@ -85,7 +88,8 @@
 //!     95_851, // Number of bits
 //!     7,      // Number of hash functions
 //! )
-//! .build();
+//! .build()
+//! .unwrap();
 //! ```
 //!
 //! # Set Operations
@@ -95,8 +99,12 @@
 //! ```
 //! use datasketches::bloom::BloomFilterBuilder;
 //!
-//! let mut filter1 = BloomFilterBuilder::with_accuracy(100, 0.01).build();
-//! let mut filter2 = BloomFilterBuilder::with_accuracy(100, 0.01).build();
+//! let mut filter1 = BloomFilterBuilder::with_accuracy(100, 0.01)
+//!     .build()
+//!     .unwrap();
+//! let mut filter2 = BloomFilterBuilder::with_accuracy(100, 0.01)
+//!     .build()
+//!     .unwrap();
 //!
 //! filter1.insert("a");
 //! filter2.insert("b");
diff --git a/datasketches/src/bloom/sketch.rs b/datasketches/src/bloom/sketch.rs
index af510f0..fbd1308 100644
--- a/datasketches/src/bloom/sketch.rs
+++ b/datasketches/src/bloom/sketch.rs
@@ -63,7 +63,9 @@ impl BloomFilter {
     /// ```
     /// use datasketches::bloom::BloomFilterBuilder;
     ///
-    /// let mut filter = BloomFilterBuilder::with_accuracy(100, 0.01).build();
+    /// let mut filter = BloomFilterBuilder::with_accuracy(100, 0.01)
+    ///     .build()
+    ///     .unwrap();
     /// filter.insert("apple");
     ///
     /// assert!(filter.contains(&"apple")); // true - possibly present (and 
known to be inserted here)
@@ -87,7 +89,9 @@ impl BloomFilter {
     /// ```
     /// use datasketches::bloom::BloomFilterBuilder;
     ///
-    /// let mut filter = BloomFilterBuilder::with_accuracy(100, 0.01).build();
+    /// let mut filter = BloomFilterBuilder::with_accuracy(100, 0.01)
+    ///     .build()
+    ///     .unwrap();
     ///
     /// let was_present = filter.contains_and_insert(&"apple");
     /// assert!(!was_present); // First insertion
@@ -111,7 +115,9 @@ impl BloomFilter {
     /// ```
     /// use datasketches::bloom::BloomFilterBuilder;
     ///
-    /// let mut filter = BloomFilterBuilder::with_accuracy(100, 0.01).build();
+    /// let mut filter = BloomFilterBuilder::with_accuracy(100, 0.01)
+    ///     .build()
+    ///     .unwrap();
     ///
     /// filter.insert("apple");
     /// filter.insert(42_u64);
@@ -133,7 +139,9 @@ impl BloomFilter {
     /// ```
     /// use datasketches::bloom::BloomFilterBuilder;
     ///
-    /// let mut filter = BloomFilterBuilder::with_accuracy(100, 0.01).build();
+    /// let mut filter = BloomFilterBuilder::with_accuracy(100, 0.01)
+    ///     .build()
+    ///     .unwrap();
     /// filter.insert("apple");
     /// assert!(!filter.is_empty());
     ///
@@ -163,10 +171,12 @@ impl BloomFilter {
     ///
     /// let mut f1 = BloomFilterBuilder::with_accuracy(100, 0.01)
     ///     .seed(123)
-    ///     .build();
+    ///     .build()
+    ///     .unwrap();
     /// let mut f2 = BloomFilterBuilder::with_accuracy(100, 0.01)
     ///     .seed(123)
-    ///     .build();
+    ///     .build()
+    ///     .unwrap();
     ///
     /// f1.insert("a");
     /// f2.insert("b");
@@ -206,10 +216,12 @@ impl BloomFilter {
     ///
     /// let mut f1 = BloomFilterBuilder::with_accuracy(100, 0.01)
     ///     .seed(123)
-    ///     .build();
+    ///     .build()
+    ///     .unwrap();
     /// let mut f2 = BloomFilterBuilder::with_accuracy(100, 0.01)
     ///     .seed(123)
-    ///     .build();
+    ///     .build()
+    ///     .unwrap();
     ///
     /// f1.insert("a");
     /// f1.insert("b");
@@ -248,7 +260,9 @@ impl BloomFilter {
     /// ```
     /// use datasketches::bloom::BloomFilterBuilder;
     ///
-    /// let mut filter = BloomFilterBuilder::with_accuracy(100, 0.01).build();
+    /// let mut filter = BloomFilterBuilder::with_accuracy(100, 0.01)
+    ///     .build()
+    ///     .unwrap();
     /// filter.insert("apple");
     ///
     /// filter.invert();
@@ -339,7 +353,9 @@ impl BloomFilter {
     /// use datasketches::bloom::BloomFilter;
     /// use datasketches::bloom::BloomFilterBuilder;
     ///
-    /// let mut filter = BloomFilterBuilder::with_accuracy(100, 0.01).build();
+    /// let mut filter = BloomFilterBuilder::with_accuracy(100, 0.01)
+    ///     .build()
+    ///     .unwrap();
     /// filter.insert("test");
     ///
     /// let bytes = filter.serialize();
@@ -404,7 +420,9 @@ impl BloomFilter {
     /// use datasketches::bloom::BloomFilter;
     /// use datasketches::bloom::BloomFilterBuilder;
     ///
-    /// let original = BloomFilterBuilder::with_accuracy(100, 0.01).build();
+    /// let original = BloomFilterBuilder::with_accuracy(100, 0.01)
+    ///     .build()
+    ///     .unwrap();
     /// let bytes = original.serialize();
     ///
     /// let restored = BloomFilter::deserialize(&bytes).unwrap();
@@ -592,13 +610,20 @@ impl BloomFilter {
 /// * [`with_accuracy()`](Self::with_accuracy): Specify target items and false 
positive rate
 ///   (recommended)
 /// * [`with_size()`](Self::with_size): Specify requested bit count and hash 
functions (manual)
+///
+/// Configuration is stored without validation and checked when 
[`build()`](Self::build) is called.
 #[derive(Debug, Clone)]
 pub struct BloomFilterBuilder {
-    num_bits: u64,
-    num_hashes: u16,
+    mode: BloomFilterBuilderMode,
     seed: u64,
 }
 
+#[derive(Debug, Clone)]
+enum BloomFilterBuilderMode {
+    Accuracy { max_items: u64, fpp: f64 },
+    Size { num_bits: u64, num_hashes: u16 },
+}
+
 impl BloomFilterBuilder {
     /// Minimum allowed requested Bloom filter size, in bits.
     pub const MIN_NUM_BITS: u64 = 1;
@@ -616,16 +641,13 @@ impl BloomFilterBuilder {
     ///
     /// Automatically calculates the optimal number of bits and hash functions
     /// to achieve the desired false positive probability for a given number 
of items.
+    /// The parameters are validated when [`build()`](Self::build) is called.
     ///
     /// # Arguments
     ///
     /// * `max_items`: Maximum expected number of distinct items.
     /// * `fpp`: Target false positive probability (for example, `0.01` for 
`1%`).
     ///
-    /// # Panics
-    ///
-    /// Panics if `max_items` is `0` or `fpp` is outside `(0.0, 1.0]`.
-    ///
     /// # Examples
     ///
     /// ```
@@ -634,21 +656,12 @@ impl BloomFilterBuilder {
     /// // Optimal for 10,000 items with 1% FPP
     /// let filter = BloomFilterBuilder::with_accuracy(10_000, 0.01)
     ///     .seed(42)
-    ///     .build();
+    ///     .build()
+    ///     .unwrap();
     /// ```
     pub fn with_accuracy(max_items: u64, fpp: f64) -> Self {
-        assert!(max_items > 0, "max_items must be greater than 0");
-        assert!(
-            fpp > 0.0 && fpp <= 1.0,
-            "fpp must be between 0.0 and 1.0 (inclusive of 1.0)"
-        );
-
-        let num_bits = Self::suggest_num_bits(max_items, fpp);
-        let num_hashes = Self::suggest_num_hashes_from_accuracy(max_items, 
num_bits);
-
         BloomFilterBuilder {
-            num_bits,
-            num_hashes,
+            mode: BloomFilterBuilderMode::Accuracy { max_items, fpp },
             seed: DEFAULT_UPDATE_SEED,
         }
     }
@@ -657,6 +670,7 @@ impl BloomFilterBuilder {
     ///
     /// Use this when you want precise control over the requested filter size,
     /// or when working with pre-calculated parameters.
+    /// The parameters are validated when [`build()`](Self::build) is called.
     ///
     /// The underlying storage is word-based, so the actual capacity is rounded
     /// up to the next multiple of 64 bits.
@@ -666,38 +680,19 @@ impl BloomFilterBuilder {
     /// * `num_bits`: Total number of bits in the filter.
     /// * `num_hashes`: Number of hash functions to use.
     ///
-    /// # Panics
-    ///
-    /// Panics if any of:
-    /// * `num_bits < Self::MIN_NUM_BITS` or `num_bits > Self::MAX_NUM_BITS`.
-    /// * `num_hashes < Self::MIN_NUM_HASHES` or `num_hashes > 
Self::MAX_NUM_HASHES`.
-    ///
     /// # Examples
     ///
     /// ```
     /// use datasketches::bloom::BloomFilterBuilder;
     ///
-    /// let filter = BloomFilterBuilder::with_size(10_000, 7).build();
+    /// let filter = BloomFilterBuilder::with_size(10_000, 7).build().unwrap();
     /// ```
     pub fn with_size(num_bits: u64, num_hashes: u16) -> Self {
-        assert!(
-            (Self::MIN_NUM_BITS..=Self::MAX_NUM_BITS).contains(&num_bits),
-            "num_bits must be between {} and {}, got {}",
-            Self::MIN_NUM_BITS,
-            Self::MAX_NUM_BITS,
-            num_bits,
-        );
-        assert!(
-            
(Self::MIN_NUM_HASHES..=Self::MAX_NUM_HASHES).contains(&num_hashes),
-            "num_hashes must be between {} and {}, got {}",
-            Self::MIN_NUM_HASHES,
-            Self::MAX_NUM_HASHES,
-            num_hashes
-        );
-
         BloomFilterBuilder {
-            num_bits,
-            num_hashes,
+            mode: BloomFilterBuilderMode::Size {
+                num_bits,
+                num_hashes,
+            },
             seed: DEFAULT_UPDATE_SEED,
         }
     }
@@ -713,7 +708,8 @@ impl BloomFilterBuilder {
     ///
     /// let filter = BloomFilterBuilder::with_accuracy(100, 0.01)
     ///     .seed(12345)
-    ///     .build();
+    ///     .build()
+    ///     .unwrap();
     /// ```
     pub fn seed(mut self, seed: u64) -> Self {
         self.seed = seed;
@@ -721,17 +717,58 @@ impl BloomFilterBuilder {
     }
 
     /// Builds the Bloom filter.
-    pub fn build(self) -> BloomFilter {
-        let num_hashes = self.num_hashes;
-        let num_words = self.num_bits.div_ceil(64) as usize;
+    ///
+    /// # Errors
+    ///
+    /// Returns an error if the configured accuracy or size parameters are 
outside their supported
+    /// ranges.
+    pub fn build(self) -> Result<BloomFilter, Error> {
+        let (num_bits, num_hashes) = match self.mode {
+            BloomFilterBuilderMode::Accuracy { max_items, fpp } => {
+                if max_items == 0 {
+                    return Err(Error::invalid_argument("max_items must be 
greater than 0"));
+                }
+                if !(fpp > 0.0 && fpp <= 1.0) {
+                    return Err(Error::invalid_argument(
+                        "fpp must be between 0.0 and 1.0 (inclusive of 1.0)",
+                    ));
+                }
+                let num_bits = Self::suggest_num_bits(max_items, fpp);
+                let num_hashes = 
Self::suggest_num_hashes_from_accuracy(max_items, num_bits);
+                (num_bits, num_hashes)
+            }
+            BloomFilterBuilderMode::Size {
+                num_bits,
+                num_hashes,
+            } => {
+                if 
!(Self::MIN_NUM_BITS..=Self::MAX_NUM_BITS).contains(&num_bits) {
+                    return Err(Error::invalid_argument(format!(
+                        "num_bits must be between {} and {}, got {}",
+                        Self::MIN_NUM_BITS,
+                        Self::MAX_NUM_BITS,
+                        num_bits
+                    )));
+                }
+                if 
!(Self::MIN_NUM_HASHES..=Self::MAX_NUM_HASHES).contains(&num_hashes) {
+                    return Err(Error::invalid_argument(format!(
+                        "num_hashes must be between {} and {}, got {}",
+                        Self::MIN_NUM_HASHES,
+                        Self::MAX_NUM_HASHES,
+                        num_hashes
+                    )));
+                }
+                (num_bits, num_hashes)
+            }
+        };
+        let num_words = num_bits.div_ceil(64) as usize;
         let bit_array = vec![0u64; num_words].into_boxed_slice();
 
-        BloomFilter {
+        Ok(BloomFilter {
             seed: self.seed,
             num_hashes,
             num_bits_set: 0,
             bit_array,
-        }
+        })
     }
 
     /// Suggests optimal number of bits given max items and target FPP.
diff --git a/datasketches/src/req/sketch.rs b/datasketches/src/req/sketch.rs
index 61d8a91..83145f0 100644
--- a/datasketches/src/req/sketch.rs
+++ b/datasketches/src/req/sketch.rs
@@ -74,6 +74,11 @@ impl<T: ReqValue> ReqSketch<T> {
     ///
     /// Panics if `k` is odd or outside `[4, 1024]`.
     pub fn new(k: u16, rank_accuracy: RankAccuracy) -> Self {
+        assert!(
+            (MIN_K..=MAX_K).contains(&k),
+            "k must be in [{MIN_K}, {MAX_K}], got {k}"
+        );
+        assert_eq!(k % 2, 0, "k must be even, got {k}");
         Self::make(k, rank_accuracy)
     }
 
@@ -734,12 +739,11 @@ impl<T: ReqValue> ReqSketch<T> {
     }
 
     fn make(k: u16, rank_accuracy: RankAccuracy) -> Self {
-        assert!(
+        debug_assert!(
             (MIN_K..=MAX_K).contains(&k),
             "k must be in [{MIN_K}, {MAX_K}], got {k}"
         );
-        assert_eq!(k % 2, 0, "k must be even, got {k}");
-
+        debug_assert_eq!(k % 2, 0, "k must be even, got {k}");
         let mut sketch = Self {
             k,
             rank_accuracy,
diff --git a/datasketches/src/tdigest/sketch.rs 
b/datasketches/src/tdigest/sketch.rs
index cfc4105..e96371f 100644
--- a/datasketches/src/tdigest/sketch.rs
+++ b/datasketches/src/tdigest/sketch.rs
@@ -204,6 +204,7 @@ impl TDigestMut {
     /// assert_eq!(sketch.k(), 100);
     /// ```
     pub fn new(k: u16) -> Self {
+        assert!(k >= 10, "k must be at least 10, got {k}");
         Self::make(
             k,
             false,
@@ -255,7 +256,7 @@ impl TDigestMut {
         buffer: TDigestBuffer,
         compressed_weight: u64,
     ) -> Self {
-        assert!(k >= 10, "k must be at least 10");
+        debug_assert!(k >= 10, "k must be at least 10");
         debug_assert!(buffer.unmerged_tail_len <= buffer.centroids.len());
         debug_assert!(buffer.compressed_prefix_len() != 0 || compressed_weight 
== 0);
 
diff --git a/datasketches/src/thetafamily/common/hash_table.rs 
b/datasketches/src/thetafamily/common/hash_table.rs
index 3661bb7..36b3d36 100644
--- a/datasketches/src/thetafamily/common/hash_table.rs
+++ b/datasketches/src/thetafamily/common/hash_table.rs
@@ -19,11 +19,13 @@ use std::hash::Hash;
 use std::slice;
 
 use crate::common::ResizeFactor;
+use crate::error::Error;
 use crate::hash::MurmurHash3X64128;
 use crate::hash::compute_seed_hash;
 use crate::thetacommon::SketchEntry;
 use crate::thetacommon::constants::HASH_TABLE_REBUILD_THRESHOLD;
 use crate::thetacommon::constants::HASH_TABLE_RESIZE_THRESHOLD;
+use crate::thetacommon::constants::MAX_LG_K;
 use crate::thetacommon::constants::MAX_THETA;
 use crate::thetacommon::constants::MIN_LG_K;
 use crate::thetacommon::constants::STRIDE_MASK;
@@ -91,12 +93,64 @@ impl<E> SketchHashTable<E>
 where
     E: SketchEntry,
 {
-    /// Create a new hash table.
+    /// Creates a new hash table.
+    ///
+    /// # Panics
+    ///
+    /// Panics if `lg_nom_size` is outside `[5, 26]` or `sampling_probability` 
is outside
+    /// `(0.0, 1.0]`.
     pub fn new(
         lg_nom_size: u8,
         resize_factor: ResizeFactor,
         sampling_probability: f32,
         seed: u64,
+    ) -> Self {
+        assert!(
+            (MIN_LG_K..=MAX_LG_K).contains(&lg_nom_size),
+            "lg_k must be in [{MIN_LG_K}, {MAX_LG_K}], got {lg_nom_size}"
+        );
+        assert!(
+            sampling_probability > 0.0 && sampling_probability <= 1.0,
+            "sampling_probability must be in (0.0, 1.0], got 
{sampling_probability}"
+        );
+        Self::make(lg_nom_size, resize_factor, sampling_probability, seed)
+    }
+
+    /// Creates a new hash table after validating its configuration.
+    ///
+    /// # Errors
+    ///
+    /// Returns an error if `lg_nom_size` is outside `[5, 26]` or 
`sampling_probability` is outside
+    /// `(0.0, 1.0]`.
+    pub fn try_new(
+        lg_nom_size: u8,
+        resize_factor: ResizeFactor,
+        sampling_probability: f32,
+        seed: u64,
+    ) -> Result<Self, Error> {
+        if !(MIN_LG_K..=MAX_LG_K).contains(&lg_nom_size) {
+            return Err(Error::invalid_argument(format!(
+                "lg_k must be in [{MIN_LG_K}, {MAX_LG_K}], got {lg_nom_size}"
+            )));
+        }
+        if !(sampling_probability > 0.0 && sampling_probability <= 1.0) {
+            return Err(Error::invalid_argument(format!(
+                "sampling_probability must be in (0.0, 1.0], got 
{sampling_probability}"
+            )));
+        }
+        Ok(Self::make(
+            lg_nom_size,
+            resize_factor,
+            sampling_probability,
+            seed,
+        ))
+    }
+
+    fn make(
+        lg_nom_size: u8,
+        resize_factor: ResizeFactor,
+        sampling_probability: f32,
+        seed: u64,
     ) -> Self {
         let lg_max_size = lg_nom_size + 1;
         let lg_cur_size = starting_sub_multiple(lg_max_size, MIN_LG_K, 
resize_factor.lg_value());
diff --git a/datasketches/src/thetafamily/common/union.rs 
b/datasketches/src/thetafamily/common/union.rs
index 5e5bbca..25b4541 100644
--- a/datasketches/src/thetafamily/common/union.rs
+++ b/datasketches/src/thetafamily/common/union.rs
@@ -61,6 +61,22 @@ where
         }
     }
 
+    /// Creates union state after validating the shared hash-table 
configuration.
+    pub fn try_new(
+        lg_k: u8,
+        resize_factor: ResizeFactor,
+        sampling_probability: f32,
+        seed: u64,
+        policy: P,
+    ) -> Result<Self, Error> {
+        let table = SketchHashTable::try_new(lg_k, resize_factor, 
sampling_probability, seed)?;
+        Ok(Self {
+            union_theta: table.theta(),
+            table,
+            policy,
+        })
+    }
+
     /// Incorporate a sketch into the union.
     pub fn update<S>(&mut self, sketch: S) -> Result<(), Error>
     where
diff --git a/datasketches/src/thetafamily/theta/a_not_b.rs 
b/datasketches/src/thetafamily/theta/a_not_b.rs
index d8edbd1..51b9791 100644
--- a/datasketches/src/thetafamily/theta/a_not_b.rs
+++ b/datasketches/src/thetafamily/theta/a_not_b.rs
@@ -39,11 +39,11 @@ use crate::thetacommon::a_not_b;
 /// use datasketches::theta::ThetaANotB;
 /// use datasketches::theta::ThetaSketchBuilder;
 ///
-/// let mut a = ThetaSketchBuilder::default().build();
+/// let mut a = ThetaSketchBuilder::default().build().unwrap();
 /// a.update("apple");
 /// a.update("banana");
 ///
-/// let mut b = ThetaSketchBuilder::default().build();
+/// let mut b = ThetaSketchBuilder::default().build().unwrap();
 /// b.update("banana");
 ///
 /// let a_not_b = ThetaANotB::default();
diff --git a/datasketches/src/thetafamily/theta/jaccard_similarity.rs 
b/datasketches/src/thetafamily/theta/jaccard_similarity.rs
index b556f86..dd932e2 100644
--- a/datasketches/src/thetafamily/theta/jaccard_similarity.rs
+++ b/datasketches/src/thetafamily/theta/jaccard_similarity.rs
@@ -34,8 +34,8 @@ use crate::thetacommon::jaccard_similarity::JaccardSimilarity;
 /// use datasketches::theta::ThetaJaccardSimilarity;
 /// use datasketches::theta::ThetaSketchBuilder;
 ///
-/// let mut a = ThetaSketchBuilder::default().build();
-/// let mut b = ThetaSketchBuilder::default().build();
+/// let mut a = ThetaSketchBuilder::default().build().unwrap();
+/// let mut b = ThetaSketchBuilder::default().build().unwrap();
 /// a.update("apple");
 /// b.update("apple");
 ///
diff --git a/datasketches/src/thetafamily/theta/mod.rs 
b/datasketches/src/thetafamily/theta/mod.rs
index 6a70a6b..c65fa09 100644
--- a/datasketches/src/thetafamily/theta/mod.rs
+++ b/datasketches/src/thetafamily/theta/mod.rs
@@ -35,7 +35,7 @@
 //! ```
 //! use datasketches::theta::ThetaSketchBuilder;
 //!
-//! let mut sketch = ThetaSketchBuilder::default().build();
+//! let mut sketch = ThetaSketchBuilder::default().build().unwrap();
 //! sketch.update("apple");
 //! assert!(sketch.estimate() >= 1.0);
 //! ```
diff --git a/datasketches/src/thetafamily/theta/sketch.rs 
b/datasketches/src/thetafamily/theta/sketch.rs
index 4293e7c..f1c9f27 100644
--- a/datasketches/src/thetafamily/theta/sketch.rs
+++ b/datasketches/src/thetafamily/theta/sketch.rs
@@ -55,9 +55,7 @@ use crate::thetacommon::constants::FLAGS_IS_COMPACT;
 use crate::thetacommon::constants::FLAGS_IS_EMPTY;
 use crate::thetacommon::constants::FLAGS_IS_ORDERED;
 use crate::thetacommon::constants::FLAGS_IS_READ_ONLY;
-use crate::thetacommon::constants::MAX_LG_K;
 use crate::thetacommon::constants::MAX_THETA;
-use crate::thetacommon::constants::MIN_LG_K;
 use crate::thetacommon::hash_table::SketchHashTableIter;
 
 /// Read-only view for Theta sketches.
@@ -71,7 +69,7 @@ use crate::thetacommon::hash_table::SketchHashTableIter;
 /// ```
 /// use datasketches::theta::ThetaSketchBuilder;
 ///
-/// let mut sketch = ThetaSketchBuilder::default().build();
+/// let mut sketch = ThetaSketchBuilder::default().build().unwrap();
 /// sketch.update("apple");
 /// let view = sketch.as_view();
 /// assert_eq!(view.num_retained(), 1);
@@ -221,11 +219,11 @@ impl ThetaSketch {
     /// use datasketches::hash::value::raw_bytes;
     /// use datasketches::theta::ThetaSketchBuilder;
     ///
-    /// let mut sketch = ThetaSketchBuilder::default().build();
+    /// let mut sketch = ThetaSketchBuilder::default().build().unwrap();
     /// sketch.update("apple");
     /// assert!(sketch.estimate() >= 1.0);
     ///
-    /// let mut sketch = ThetaSketchBuilder::default().build();
+    /// let mut sketch = ThetaSketchBuilder::default().build().unwrap();
     /// sketch.update(raw_bytes::from_str("apple"));
     /// assert!(sketch.estimate() >= 1.0);
     /// ```
@@ -240,7 +238,7 @@ impl ThetaSketch {
     /// ```
     /// use datasketches::theta::ThetaSketchBuilder;
     ///
-    /// let mut sketch = ThetaSketchBuilder::default().build();
+    /// let mut sketch = ThetaSketchBuilder::default().build().unwrap();
     /// sketch.update("apple");
     /// assert!(sketch.estimate() >= 1.0);
     /// ```
@@ -305,7 +303,7 @@ impl ThetaSketch {
     /// ```
     /// use datasketches::theta::ThetaSketchBuilder;
     ///
-    /// let mut sketch = ThetaSketchBuilder::default().build();
+    /// let mut sketch = ThetaSketchBuilder::default().build().unwrap();
     /// sketch.update("apple");
     /// let mut iter = sketch.iter();
     /// assert!(iter.next().is_some());
@@ -323,7 +321,7 @@ impl ThetaSketch {
     /// ```
     /// use datasketches::theta::ThetaSketchBuilder;
     ///
-    /// let mut sketch = ThetaSketchBuilder::default().build();
+    /// let mut sketch = ThetaSketchBuilder::default().build().unwrap();
     /// sketch.update("apple");
     /// let compact = sketch.compact(true);
     /// assert_eq!(compact.num_retained(), 1);
@@ -355,7 +353,7 @@ impl ThetaSketch {
     /// use datasketches::common::NumStdDev;
     /// use datasketches::theta::ThetaSketchBuilder;
     ///
-    /// let mut sketch = ThetaSketchBuilder::default().lg_k(12).build();
+    /// let mut sketch = 
ThetaSketchBuilder::default().lg_k(12).build().unwrap();
     /// for i in 0..10000 {
     ///     sketch.update(i);
     /// }
@@ -389,7 +387,7 @@ impl ThetaSketch {
     /// use datasketches::common::NumStdDev;
     /// use datasketches::theta::ThetaSketchBuilder;
     ///
-    /// let mut sketch = ThetaSketchBuilder::default().lg_k(12).build();
+    /// let mut sketch = 
ThetaSketchBuilder::default().lg_k(12).build().unwrap();
     /// for i in 0..10000 {
     ///     sketch.update(i);
     /// }
@@ -1018,6 +1016,8 @@ impl CompactThetaSketch {
 }
 
 /// Builder for [`ThetaSketch`].
+///
+/// Configuration is stored without validation and checked when 
[`build()`](Self::build) is called.
 #[derive(Debug)]
 pub struct ThetaSketchBuilder {
     lg_k: u8,
@@ -1040,26 +1040,15 @@ impl Default for ThetaSketchBuilder {
 impl ThetaSketchBuilder {
     /// Sets `lg_k`, the base-2 logarithm of the nominal capacity.
     ///
-    /// # Panics
-    ///
-    /// Panics if `lg_k` is outside `[5, 26]`.
-    ///
     /// # Examples
     ///
     /// ```
     /// use datasketches::theta::ThetaSketchBuilder;
     ///
-    /// let sketch = ThetaSketchBuilder::default().lg_k(12).build();
+    /// let sketch = ThetaSketchBuilder::default().lg_k(12).build().unwrap();
     /// assert_eq!(sketch.lg_k(), 12);
     /// ```
     pub fn lg_k(mut self, lg_k: u8) -> Self {
-        assert!(
-            (MIN_LG_K..=MAX_LG_K).contains(&lg_k),
-            "lg_k must be in [{}, {}], got {}",
-            MIN_LG_K,
-            MAX_LG_K,
-            lg_k
-        );
         self.lg_k = lg_k;
         self
     }
@@ -1075,10 +1064,6 @@ impl ThetaSketchBuilder {
     /// The sampling probability controls the fraction of hashed values that 
are retained.
     /// It must be greater than `0.0` to ensure valid theta values for bound 
calculations.
     ///
-    /// # Panics
-    ///
-    /// Panics if `probability` is outside `(0.0, 1.0]`.
-    ///
     /// # Examples
     ///
     /// ```
@@ -1086,13 +1071,10 @@ impl ThetaSketchBuilder {
     ///
     /// ThetaSketchBuilder::default()
     ///     .sampling_probability(0.5)
-    ///     .build();
+    ///     .build()
+    ///     .unwrap();
     /// ```
     pub fn sampling_probability(mut self, probability: f32) -> Self {
-        assert!(
-            (0.0..=1.0).contains(&probability) && probability > 0.0,
-            "sampling_probability must be in (0.0, 1.0], got {probability}"
-        );
         self.sampling_probability = probability;
         self
     }
@@ -1104,7 +1086,7 @@ impl ThetaSketchBuilder {
     /// ```
     /// use datasketches::theta::ThetaSketchBuilder;
     ///
-    /// ThetaSketchBuilder::default().seed(7).build();
+    /// ThetaSketchBuilder::default().seed(7).build().unwrap();
     /// ```
     pub fn seed(mut self, seed: u64) -> Self {
         self.seed = seed;
@@ -1113,22 +1095,27 @@ impl ThetaSketchBuilder {
 
     /// Builds the [`ThetaSketch`].
     ///
+    /// # Errors
+    ///
+    /// Returns an error if `lg_k` is outside `[5, 26]` or 
`sampling_probability` is outside
+    /// `(0.0, 1.0]`.
+    ///
     /// # Examples
     ///
     /// ```
     /// use datasketches::theta::ThetaSketchBuilder;
     ///
-    /// ThetaSketchBuilder::default().lg_k(10).build();
+    /// ThetaSketchBuilder::default().lg_k(10).build().unwrap();
     /// ```
-    pub fn build(self) -> ThetaSketch {
-        let table = ThetaHashTable::new(
+    pub fn build(self) -> Result<ThetaSketch, Error> {
+        let table = ThetaHashTable::try_new(
             self.lg_k,
             self.resize_factor,
             self.sampling_probability,
             self.seed,
-        );
+        )?;
 
-        ThetaSketch { table }
+        Ok(ThetaSketch { table })
     }
 }
 
@@ -1190,7 +1177,7 @@ mod tests {
 
     #[test]
     fn theta_and_compact_theta_equivalent() {
-        let mut exact_theta = ThetaSketchBuilder::default().lg_k(12).build();
+        let mut exact_theta = 
ThetaSketchBuilder::default().lg_k(12).build().unwrap();
         for i in 0..2000 {
             exact_theta.update(i);
         }
@@ -1199,7 +1186,7 @@ mod tests {
             assert_theta_and_compact_equivalent_ordered(&exact_theta, ordered);
         }
 
-        let mut estimation_theta = 
ThetaSketchBuilder::default().lg_k(5).build();
+        let mut estimation_theta = 
ThetaSketchBuilder::default().lg_k(5).build().unwrap();
         for i in 0..5000 {
             estimation_theta.update(i);
         }
@@ -1211,7 +1198,7 @@ mod tests {
 
     #[test]
     fn 
compact_theta_serialize_deserialize_round_trip_equivalent_to_compact_and_theta()
 {
-        let mut theta = ThetaSketchBuilder::default().lg_k(5).build();
+        let mut theta = ThetaSketchBuilder::default().lg_k(5).build().unwrap();
         for i in 0..5000 {
             theta.update(i);
         }
@@ -1227,7 +1214,7 @@ mod tests {
 
     #[test]
     fn compact_theta_serialize_compressed_round_trip_tail_entries() {
-        let mut theta = ThetaSketchBuilder::default().lg_k(12).build();
+        let mut theta = 
ThetaSketchBuilder::default().lg_k(12).build().unwrap();
         for i in 0..13 {
             theta.update(i);
         }
@@ -1242,7 +1229,7 @@ mod tests {
 
     #[test]
     fn compact_theta_serialize_compressed_round_trip_more_than_255_entries() {
-        let mut theta = ThetaSketchBuilder::default().lg_k(12).build();
+        let mut theta = 
ThetaSketchBuilder::default().lg_k(12).build().unwrap();
         for i in 0..300 {
             theta.update(i);
         }
@@ -1257,7 +1244,7 @@ mod tests {
 
     #[test]
     fn compact_theta_serialize_compressed_round_trip_estimation_mode() {
-        let mut theta = ThetaSketchBuilder::default().lg_k(5).build();
+        let mut theta = ThetaSketchBuilder::default().lg_k(5).build().unwrap();
         for i in 0..5000 {
             theta.update(i);
         }
diff --git a/datasketches/src/thetafamily/theta/union.rs 
b/datasketches/src/thetafamily/theta/union.rs
index d10b1d2..956e6c6 100644
--- a/datasketches/src/thetafamily/theta/union.rs
+++ b/datasketches/src/thetafamily/theta/union.rs
@@ -22,8 +22,6 @@ use crate::theta::CompactThetaSketch;
 use crate::theta::ThetaSketchView;
 use crate::theta::hash_table::ThetaEntry;
 use crate::thetacommon::constants::DEFAULT_LG_K;
-use crate::thetacommon::constants::MAX_LG_K;
-use crate::thetacommon::constants::MIN_LG_K;
 use crate::thetacommon::union::UnionMergePolicy;
 use crate::thetacommon::union::UnionState;
 
@@ -75,6 +73,8 @@ impl ThetaUnion {
 }
 
 /// Builder for [`ThetaUnion`].
+///
+/// Configuration is stored without validation and checked when 
[`build()`](Self::build) is called.
 #[derive(Debug, Clone)]
 pub struct ThetaUnionBuilder {
     lg_k: u8,
@@ -97,22 +97,14 @@ impl Default for ThetaUnionBuilder {
 impl ThetaUnionBuilder {
     /// Sets `lg_k`, the base-2 logarithm of the nominal capacity.
     ///
-    /// # Panics
-    ///
-    /// Panics if `lg_k` is outside `[5, 26]`.
-    ///
     /// # Examples
     ///
     /// ```
     /// use datasketches::theta::ThetaUnionBuilder;
     ///
-    /// ThetaUnionBuilder::default().lg_k(12).build();
+    /// ThetaUnionBuilder::default().lg_k(12).build().unwrap();
     /// ```
     pub fn lg_k(mut self, lg_k: u8) -> Self {
-        assert!(
-            (MIN_LG_K..=MAX_LG_K).contains(&lg_k),
-            "lg_k must be in [{MIN_LG_K}, {MAX_LG_K}], got {lg_k}"
-        );
         self.lg_k = lg_k;
         self
     }
@@ -125,10 +117,6 @@ impl ThetaUnionBuilder {
 
     /// Sets the sampling probability.
     ///
-    /// # Panics
-    ///
-    /// Panics if `probability` is outside `(0.0, 1.0]`.
-    ///
     /// # Examples
     ///
     /// ```
@@ -136,13 +124,10 @@ impl ThetaUnionBuilder {
     ///
     /// ThetaUnionBuilder::default()
     ///     .sampling_probability(0.5)
-    ///     .build();
+    ///     .build()
+    ///     .unwrap();
     /// ```
     pub fn sampling_probability(mut self, probability: f32) -> Self {
-        assert!(
-            (0.0..=1.0).contains(&probability) && probability > 0.0,
-            "sampling_probability must be in (0.0, 1.0], got {probability}"
-        );
         self.sampling_probability = probability;
         self
     }
@@ -154,7 +139,7 @@ impl ThetaUnionBuilder {
     /// ```
     /// use datasketches::theta::ThetaUnionBuilder;
     ///
-    /// ThetaUnionBuilder::default().seed(7).build();
+    /// ThetaUnionBuilder::default().seed(7).build().unwrap();
     /// ```
     pub fn seed(mut self, seed: u64) -> Self {
         self.seed = seed;
@@ -163,22 +148,27 @@ impl ThetaUnionBuilder {
 
     /// Builds the [`ThetaUnion`].
     ///
+    /// # Errors
+    ///
+    /// Returns an error if `lg_k` is outside `[5, 26]` or 
`sampling_probability` is outside
+    /// `(0.0, 1.0]`.
+    ///
     /// # Examples
     ///
     /// ```
     /// use datasketches::theta::ThetaUnionBuilder;
     ///
-    /// ThetaUnionBuilder::default().lg_k(10).build();
+    /// ThetaUnionBuilder::default().lg_k(10).build().unwrap();
     /// ```
-    pub fn build(self) -> ThetaUnion {
-        ThetaUnion {
-            state: UnionState::new(
+    pub fn build(self) -> Result<ThetaUnion, Error> {
+        Ok(ThetaUnion {
+            state: UnionState::try_new(
                 self.lg_k,
                 self.resize_factor,
                 self.sampling_probability,
                 self.seed,
                 NoopUnionPolicy,
-            ),
-        }
+            )?,
+        })
     }
 }
diff --git a/datasketches/src/thetafamily/tuple/a_not_b.rs 
b/datasketches/src/thetafamily/tuple/a_not_b.rs
index dbe6c3b..914c220 100644
--- a/datasketches/src/thetafamily/tuple/a_not_b.rs
+++ b/datasketches/src/thetafamily/tuple/a_not_b.rs
@@ -43,11 +43,11 @@ use crate::tuple::sketch::TupleSketchView;
 /// use datasketches::tuple::TupleSketchBuilder;
 ///
 /// let update_policy = DefaultUpdatePolicy::<u64>::default();
-/// let mut a = TupleSketchBuilder::new(update_policy).build();
+/// let mut a = TupleSketchBuilder::new(update_policy).build().unwrap();
 /// a.update("apple", 1);
 /// a.update("banana", 1);
 ///
-/// let mut b = TupleSketchBuilder::new(update_policy).build();
+/// let mut b = TupleSketchBuilder::new(update_policy).build().unwrap();
 /// b.update("banana", 1);
 ///
 /// let a_not_b = TupleANotB::default();
diff --git a/datasketches/src/thetafamily/tuple/intersection.rs 
b/datasketches/src/thetafamily/tuple/intersection.rs
index c337f33..0d0833e 100644
--- a/datasketches/src/thetafamily/tuple/intersection.rs
+++ b/datasketches/src/thetafamily/tuple/intersection.rs
@@ -68,11 +68,11 @@ use crate::tuple::sketch::TupleSketchView;
 /// }
 ///
 /// let update_policy = DefaultUpdatePolicy::<u64>::default();
-/// let mut a = TupleSketchBuilder::new(update_policy).build();
+/// let mut a = TupleSketchBuilder::new(update_policy).build().unwrap();
 /// a.update("shared", 3);
 /// a.update("only_a", 1);
 ///
-/// let mut b = TupleSketchBuilder::new(update_policy).build();
+/// let mut b = TupleSketchBuilder::new(update_policy).build().unwrap();
 /// b.update("shared", 4);
 /// b.update("only_b", 1);
 ///
diff --git a/datasketches/src/thetafamily/tuple/jaccard_similarity.rs 
b/datasketches/src/thetafamily/tuple/jaccard_similarity.rs
index e65becb..d2fcc66 100644
--- a/datasketches/src/thetafamily/tuple/jaccard_similarity.rs
+++ b/datasketches/src/thetafamily/tuple/jaccard_similarity.rs
@@ -37,8 +37,8 @@ use crate::tuple::TupleSketchView;
 /// use datasketches::tuple::TupleSketchBuilder;
 ///
 /// let policy = DefaultUpdatePolicy::<u64>::default();
-/// let mut a = TupleSketchBuilder::new(policy).build();
-/// let mut b = TupleSketchBuilder::new(policy).build();
+/// let mut a = TupleSketchBuilder::new(policy).build().unwrap();
+/// let mut b = TupleSketchBuilder::new(policy).build().unwrap();
 /// a.update("apple", 1);
 /// b.update("apple", 2);
 ///
diff --git a/datasketches/src/thetafamily/tuple/mod.rs 
b/datasketches/src/thetafamily/tuple/mod.rs
index c159d35..c057ee5 100644
--- a/datasketches/src/thetafamily/tuple/mod.rs
+++ b/datasketches/src/thetafamily/tuple/mod.rs
@@ -35,7 +35,7 @@
 //! use datasketches::tuple::TupleSketchBuilder;
 //!
 //! let policy = DefaultUpdatePolicy::<u64>::default();
-//! let mut sketch = TupleSketchBuilder::new(policy).build();
+//! let mut sketch = TupleSketchBuilder::new(policy).build().unwrap();
 //! sketch.update("apple", 1_u64);
 //! assert!(sketch.estimate() >= 1.0);
 //! ```
diff --git a/datasketches/src/thetafamily/tuple/sketch.rs 
b/datasketches/src/thetafamily/tuple/sketch.rs
index fd1c8fb..ada2244 100644
--- a/datasketches/src/thetafamily/tuple/sketch.rs
+++ b/datasketches/src/thetafamily/tuple/sketch.rs
@@ -46,9 +46,7 @@ use crate::thetacommon::constants::FLAGS_IS_COMPACT;
 use crate::thetacommon::constants::FLAGS_IS_EMPTY;
 use crate::thetacommon::constants::FLAGS_IS_ORDERED;
 use crate::thetacommon::constants::FLAGS_IS_READ_ONLY;
-use crate::thetacommon::constants::MAX_LG_K;
 use crate::thetacommon::constants::MAX_THETA;
-use crate::thetacommon::constants::MIN_LG_K;
 use crate::thetacommon::hash_table::SketchHashTableIter;
 use crate::tuple::hash_table::TupleEntry;
 use crate::tuple::hash_table::TupleHashTable;
@@ -71,7 +69,9 @@ use crate::tuple::serialization::TupleSummaryValue;
 /// use datasketches::tuple::DefaultUpdatePolicy;
 /// use datasketches::tuple::TupleSketchBuilder;
 ///
-/// let mut sketch = 
TupleSketchBuilder::new(DefaultUpdatePolicy::<u64>::default()).build();
+/// let mut sketch = 
TupleSketchBuilder::new(DefaultUpdatePolicy::<u64>::default())
+///     .build()
+///     .unwrap();
 /// sketch.update("apple", 1);
 /// let view = sketch.as_view();
 /// assert_eq!(view.iter().next().unwrap().1, &1);
@@ -232,7 +232,7 @@ impl<'a, S> From<&'a CompactTupleSketch<S>> for 
TupleSketchView<'a, S> {
 /// use datasketches::tuple::TupleSketchBuilder;
 ///
 /// let policy = DefaultUpdatePolicy::<u64>::default();
-/// let mut sketch = TupleSketchBuilder::new(policy).build();
+/// let mut sketch = TupleSketchBuilder::new(policy).build().unwrap();
 /// sketch.update("apple", 1);
 /// sketch.update("apple", 1);
 /// assert!(sketch.estimate() >= 1.0);
@@ -269,7 +269,7 @@ where
     /// use datasketches::tuple::TupleSketchBuilder;
     ///
     /// let policy = DefaultUpdatePolicy::<u64>::default();
-    /// let mut sketch = TupleSketchBuilder::new(policy).build();
+    /// let mut sketch = TupleSketchBuilder::new(policy).build().unwrap();
     /// sketch.update(42, 5);
     /// ```
     pub fn update<U>(&mut self, key: impl Hash, value: U)
@@ -395,7 +395,7 @@ where
     /// use datasketches::tuple::TupleSketchBuilder;
     ///
     /// let policy = DefaultUpdatePolicy::<u64>::default();
-    /// let mut sketch = TupleSketchBuilder::new(policy).build();
+    /// let mut sketch = TupleSketchBuilder::new(policy).build().unwrap();
     /// sketch.update("apple", 1);
     /// let compact = sketch.compact(true);
     /// assert_eq!(compact.num_retained(), 1);
@@ -554,7 +554,7 @@ impl<S> CompactTupleSketch<S> {
     /// use datasketches::tuple::TupleSketchBuilder;
     ///
     /// let policy = DefaultUpdatePolicy::<u64>::default();
-    /// let mut sketch = TupleSketchBuilder::new(policy).build();
+    /// let mut sketch = TupleSketchBuilder::new(policy).build().unwrap();
     /// sketch.update("apple", 1);
     /// let bytes = sketch.compact(true).serialize();
     /// assert!(!bytes.is_empty());
@@ -719,6 +719,8 @@ impl<S> CompactTupleSketch<S> {
 /// Every builder carries a concrete [`SummaryPolicy`]. Use
 /// [`DefaultUpdatePolicy`](crate::tuple::DefaultUpdatePolicy) for 
default-constructed additive
 /// summaries, or supply a custom policy.
+///
+/// Configuration is stored without validation and checked when 
[`build()`](Self::build) is called.
 #[derive(Debug)]
 pub struct TupleSketchBuilder<P>
 where
@@ -760,7 +762,7 @@ where
     ///     }
     /// }
     ///
-    /// let mut sketch = TupleSketchBuilder::new(MaxPolicy).build();
+    /// let mut sketch = TupleSketchBuilder::new(MaxPolicy).build().unwrap();
     /// sketch.update("k", 3);
     /// sketch.update("k", 7);
     /// ```
@@ -775,15 +777,7 @@ where
     }
 
     /// Sets `lg_k`, the base-2 logarithm of the nominal capacity.
-    ///
-    /// # Panics
-    ///
-    /// Panics if `lg_k` is outside `[5, 26]`.
     pub fn lg_k(mut self, lg_k: u8) -> Self {
-        assert!(
-            (MIN_LG_K..=MAX_LG_K).contains(&lg_k),
-            "lg_k must be in [{MIN_LG_K}, {MAX_LG_K}], got {lg_k}"
-        );
         self.lg_k = lg_k;
         self
     }
@@ -795,15 +789,7 @@ where
     }
 
     /// Sets the sampling probability.
-    ///
-    /// # Panics
-    ///
-    /// Panics if `probability` is outside `(0.0, 1.0]`.
     pub fn sampling_probability(mut self, probability: f32) -> Self {
-        assert!(
-            (0.0..=1.0).contains(&probability) && probability > 0.0,
-            "sampling_probability must be in (0.0, 1.0], got {probability}"
-        );
         self.sampling_probability = probability;
         self
     }
@@ -815,15 +801,20 @@ where
     }
 
     /// Builds a [`TupleSketch`] using the supplied policy.
-    pub fn build(self) -> TupleSketch<P> {
-        TupleSketch {
-            table: TupleHashTable::new(
+    ///
+    /// # Errors
+    ///
+    /// Returns an error if `lg_k` is outside `[5, 26]` or 
`sampling_probability` is outside
+    /// `(0.0, 1.0]`.
+    pub fn build(self) -> Result<TupleSketch<P>, Error> {
+        Ok(TupleSketch {
+            table: TupleHashTable::try_new(
                 self.lg_k,
                 self.resize_factor,
                 self.sampling_probability,
                 self.seed,
-            ),
+            )?,
             policy: self.policy,
-        }
+        })
     }
 }
diff --git a/datasketches/src/thetafamily/tuple/union.rs 
b/datasketches/src/thetafamily/tuple/union.rs
index 25bef18..ddea80d 100644
--- a/datasketches/src/thetafamily/tuple/union.rs
+++ b/datasketches/src/thetafamily/tuple/union.rs
@@ -26,8 +26,6 @@ use crate::common::ResizeFactor;
 use crate::error::Error;
 use crate::hash::DEFAULT_UPDATE_SEED;
 use crate::thetacommon::constants::DEFAULT_LG_K;
-use crate::thetacommon::constants::MAX_LG_K;
-use crate::thetacommon::constants::MIN_LG_K;
 use crate::thetacommon::union::UnionState;
 use crate::tuple::hash_table::TupleEntry;
 use crate::tuple::policy::SummaryCombinePolicy;
@@ -49,15 +47,17 @@ use crate::tuple::sketch::TupleSketchView;
 /// use datasketches::tuple::TupleUnionBuilder;
 ///
 /// let update_policy = DefaultUpdatePolicy::<u64>::default();
-/// let mut a = TupleSketchBuilder::new(update_policy).build();
+/// let mut a = TupleSketchBuilder::new(update_policy).build().unwrap();
 /// a.update("apple", 1);
 /// a.update("banana", 1);
 ///
-/// let mut b = TupleSketchBuilder::new(update_policy).build();
+/// let mut b = TupleSketchBuilder::new(update_policy).build().unwrap();
 /// b.update("banana", 1);
 /// b.update("cherry", 1);
 ///
-/// let mut union = 
TupleUnionBuilder::new(DefaultUnionPolicy::<u64>::default()).build();
+/// let mut union = 
TupleUnionBuilder::new(DefaultUnionPolicy::<u64>::default())
+///     .build()
+///     .unwrap();
 /// union.update(&a).unwrap();
 /// union.update(&b).unwrap();
 ///
@@ -130,6 +130,8 @@ where
 /// Every builder carries a concrete [`SummaryCombinePolicy`]. Use
 /// [`DefaultUnionPolicy`](crate::tuple::DefaultUnionPolicy) for additive 
summaries, or supply a
 /// custom combine policy.
+///
+/// Configuration is stored without validation and checked when 
[`build()`](Self::build) is called.
 #[derive(Debug)]
 pub struct TupleUnionBuilder<P>
 where
@@ -156,7 +158,8 @@ where
     ///
     /// let union = 
TupleUnionBuilder::new(DefaultUnionPolicy::<u64>::default())
     ///     .lg_k(12)
-    ///     .build();
+    ///     .build()
+    ///     .unwrap();
     /// ```
     pub fn new(policy: P) -> Self {
         Self {
@@ -169,15 +172,7 @@ where
     }
 
     /// Sets `lg_k`, the base-2 logarithm of the nominal capacity.
-    ///
-    /// # Panics
-    ///
-    /// Panics if `lg_k` is outside `[5, 26]`.
     pub fn lg_k(mut self, lg_k: u8) -> Self {
-        assert!(
-            (MIN_LG_K..=MAX_LG_K).contains(&lg_k),
-            "lg_k must be in [{MIN_LG_K}, {MAX_LG_K}], got {lg_k}"
-        );
         self.lg_k = lg_k;
         self
     }
@@ -189,15 +184,7 @@ where
     }
 
     /// Sets the sampling probability.
-    ///
-    /// # Panics
-    ///
-    /// Panics if `probability` is outside `(0.0, 1.0]`.
     pub fn sampling_probability(mut self, probability: f32) -> Self {
-        assert!(
-            (0.0..=1.0).contains(&probability) && probability > 0.0,
-            "sampling_probability must be in (0.0, 1.0], got {probability}"
-        );
         self.sampling_probability = probability;
         self
     }
@@ -209,15 +196,20 @@ where
     }
 
     /// Builds the [`TupleUnion`].
-    pub fn build(self) -> TupleUnion<P> {
-        TupleUnion {
-            state: UnionState::new(
+    ///
+    /// # Errors
+    ///
+    /// Returns an error if `lg_k` is outside `[5, 26]` or 
`sampling_probability` is outside
+    /// `(0.0, 1.0]`.
+    pub fn build(self) -> Result<TupleUnion<P>, Error> {
+        Ok(TupleUnion {
+            state: UnionState::try_new(
                 self.lg_k,
                 self.resize_factor,
                 self.sampling_probability,
                 self.seed,
                 self.policy,
-            ),
-        }
+            )?,
+        })
     }
 }
diff --git a/tests-integration/tests/bloom_test/sketch.rs 
b/tests-integration/tests/bloom_test/sketch.rs
index e164140..15f44a9 100644
--- a/tests-integration/tests/bloom_test/sketch.rs
+++ b/tests-integration/tests/bloom_test/sketch.rs
@@ -16,6 +16,7 @@
 // under the License.
 
 use datasketches::bloom::BloomFilterBuilder;
+use datasketches::error::ErrorKind;
 use googletest::assert_that;
 use googletest::prelude::ge;
 use googletest::prelude::gt;
@@ -29,6 +30,7 @@ fn filter() -> datasketches::bloom::BloomFilter {
     BloomFilterBuilder::with_size(NUM_BITS, NUM_HASHES)
         .seed(SEED)
         .build()
+        .unwrap()
 }
 
 #[test]
@@ -103,13 +105,16 @@ fn test_compatibility_checks_all_configuration() {
 
     let different_seed = BloomFilterBuilder::with_size(NUM_BITS, NUM_HASHES)
         .seed(SEED + 1)
-        .build();
+        .build()
+        .unwrap();
     let different_size = BloomFilterBuilder::with_size(NUM_BITS * 2, 
NUM_HASHES)
         .seed(SEED)
-        .build();
+        .build()
+        .unwrap();
     let different_hashes = BloomFilterBuilder::with_size(NUM_BITS, NUM_HASHES 
+ 1)
         .seed(SEED)
-        .build();
+        .build()
+        .unwrap();
 
     assert!(!baseline.is_compatible(&different_seed));
     assert!(!baseline.is_compatible(&different_size));
@@ -122,7 +127,8 @@ fn test_union_rejects_incompatible_filters() {
     let mut left = filter();
     let right = BloomFilterBuilder::with_size(NUM_BITS, NUM_HASHES)
         .seed(SEED + 1)
-        .build();
+        .build()
+        .unwrap();
     left.union(&right);
 }
 
@@ -132,36 +138,41 @@ fn test_intersection_rejects_incompatible_filters() {
     let mut left = filter();
     let right = BloomFilterBuilder::with_size(NUM_BITS, NUM_HASHES)
         .seed(SEED + 1)
-        .build();
+        .build()
+        .unwrap();
     left.intersect(&right);
 }
 
 #[test]
 fn test_requested_size_rounds_to_word_boundary() {
-    let filter = BloomFilterBuilder::with_size(65, 3).build();
+    let filter = BloomFilterBuilder::with_size(65, 3).build().unwrap();
     assert_eq!(filter.capacity(), 128);
 }
 
 #[test]
-#[should_panic(expected = "max_items must be greater than 0")]
-fn test_accuracy_builder_rejects_zero_items() {
-    BloomFilterBuilder::with_accuracy(0, 0.01);
+fn test_accuracy_builder_rejects_zero_items_at_build() {
+    let error = BloomFilterBuilder::with_accuracy(0, 0.01)
+        .build()
+        .unwrap_err();
+    assert_eq!(error.kind(), ErrorKind::InvalidArgument);
 }
 
 #[test]
-#[should_panic(expected = "fpp must be between")]
-fn test_accuracy_builder_rejects_invalid_probability() {
-    BloomFilterBuilder::with_accuracy(100, 1.5);
+fn test_accuracy_builder_rejects_invalid_probability_at_build() {
+    let error = BloomFilterBuilder::with_accuracy(100, 1.5)
+        .build()
+        .unwrap_err();
+    assert_eq!(error.kind(), ErrorKind::InvalidArgument);
 }
 
 #[test]
-#[should_panic(expected = "num_bits must be between")]
-fn test_size_builder_rejects_zero_bits() {
-    BloomFilterBuilder::with_size(0, 3);
+fn test_size_builder_rejects_zero_bits_at_build() {
+    let error = BloomFilterBuilder::with_size(0, 3).build().unwrap_err();
+    assert_eq!(error.kind(), ErrorKind::InvalidArgument);
 }
 
 #[test]
-#[should_panic(expected = "num_hashes must be between")]
-fn test_size_builder_rejects_zero_hashes() {
-    BloomFilterBuilder::with_size(128, 0);
+fn test_size_builder_rejects_zero_hashes_at_build() {
+    let error = BloomFilterBuilder::with_size(128, 0).build().unwrap_err();
+    assert_eq!(error.kind(), ErrorKind::InvalidArgument);
 }
diff --git a/tests-integration/tests/serde_tests/bloom.rs 
b/tests-integration/tests/serde_tests/bloom.rs
index 708c7f7..4c3e712 100644
--- a/tests-integration/tests/serde_tests/bloom.rs
+++ b/tests-integration/tests/serde_tests/bloom.rs
@@ -180,7 +180,9 @@ fn test_go_compatibility() {
 fn test_cached_num_bits_set_is_validated_or_recomputed() {
     const NUM_BITS_SET_OFFSET: usize = 24;
 
-    let mut filter = BloomFilterBuilder::with_accuracy(100, 0.01).build();
+    let mut filter = BloomFilterBuilder::with_accuracy(100, 0.01)
+        .build()
+        .unwrap();
     filter.insert("apple");
     filter.insert("banana");
     let actual_bits_set = filter.bits_used();
@@ -207,7 +209,9 @@ fn test_cached_num_bits_set_is_validated_or_recomputed() {
 fn test_nonempty_payload_length_is_checked_before_allocating() {
     const NUM_LONGS_OFFSET: usize = 16;
 
-    let mut filter = BloomFilterBuilder::with_accuracy(100, 0.01).build();
+    let mut filter = BloomFilterBuilder::with_accuracy(100, 0.01)
+        .build()
+        .unwrap();
     filter.insert("apple");
     let mut bytes = filter.serialize();
     bytes[NUM_LONGS_OFFSET..NUM_LONGS_OFFSET + size_of::<i32>()]
diff --git a/tests-integration/tests/serde_tests/theta.rs 
b/tests-integration/tests/serde_tests/theta.rs
index e1ee343..0c1342f 100644
--- a/tests-integration/tests/serde_tests/theta.rs
+++ b/tests-integration/tests/serde_tests/theta.rs
@@ -29,7 +29,7 @@ use googletest::prelude::near;
 use crate::serialization_test_data;
 
 fn serialize_v2_exact(entries: &[u64]) -> Vec<u8> {
-    let current = ThetaSketchBuilder::default().build().compact(true);
+    let current = ThetaSketchBuilder::default().build().unwrap().compact(true);
     let current_bytes = current.serialize();
     let mut bytes = SketchBytes::with_capacity((2 + entries.len()) * 
size_of::<u64>());
     bytes.write_u8(2); // preamble longs
@@ -160,7 +160,7 @@ fn test_go_compatibility() {
 
 #[test]
 fn malformed_input_is_rejected() {
-    let mut sketch = ThetaSketchBuilder::default().lg_k(5).build();
+    let mut sketch = ThetaSketchBuilder::default().lg_k(5).build().unwrap();
     for value in 0..5000 {
         sketch.update(value);
     }
@@ -190,7 +190,7 @@ fn declared_entry_payload_is_checked_before_allocating() {
     uncompressed[8..12].copy_from_slice(&u32::MAX.to_le_bytes());
     assert!(CompactThetaSketch::deserialize(&uncompressed).is_err());
 
-    let mut sketch = ThetaSketchBuilder::default().lg_k(5).build();
+    let mut sketch = ThetaSketchBuilder::default().lg_k(5).build().unwrap();
     for value in 0..5000 {
         sketch.update(value);
     }
diff --git a/tests-integration/tests/serde_tests/tuple.rs 
b/tests-integration/tests/serde_tests/tuple.rs
index 33103f0..2b73c0b 100644
--- a/tests-integration/tests/serde_tests/tuple.rs
+++ b/tests-integration/tests/serde_tests/tuple.rs
@@ -109,7 +109,9 @@ fn test_go_compatibility() {
 
 #[test]
 fn round_trip_preserves_summaries() {
-    let mut sketch = 
TupleSketchBuilder::new(DefaultUpdatePolicy::<u64>::default()).build();
+    let mut sketch = 
TupleSketchBuilder::new(DefaultUpdatePolicy::<u64>::default())
+        .build()
+        .unwrap();
     for value in 0..50 {
         sketch.update(value, 1);
         sketch.update(value, 2);
@@ -125,7 +127,9 @@ fn round_trip_preserves_summaries() {
 
 #[test]
 fn malformed_input_is_rejected() {
-    let mut sketch = 
TupleSketchBuilder::new(DefaultUpdatePolicy::<u64>::default()).build();
+    let mut sketch = 
TupleSketchBuilder::new(DefaultUpdatePolicy::<u64>::default())
+        .build()
+        .unwrap();
     for value in 0..100 {
         sketch.update(value, 1);
     }
@@ -146,7 +150,9 @@ fn malformed_input_is_rejected() {
 
 #[test]
 fn declared_entry_payload_is_checked_before_allocating() {
-    let mut sketch = 
TupleSketchBuilder::new(DefaultUpdatePolicy::<u64>::default()).build();
+    let mut sketch = 
TupleSketchBuilder::new(DefaultUpdatePolicy::<u64>::default())
+        .build()
+        .unwrap();
     for value in 0..100 {
         sketch.update(value, 1);
     }
diff --git a/tests-integration/tests/theta_test/a_not_b.rs 
b/tests-integration/tests/theta_test/a_not_b.rs
index bc98820..d5918ca 100644
--- a/tests-integration/tests/theta_test/a_not_b.rs
+++ b/tests-integration/tests/theta_test/a_not_b.rs
@@ -31,7 +31,7 @@ use googletest::prelude::lt;
 use googletest::prelude::near;
 
 fn sketch_with_range(start: u64, count: u64) -> ThetaSketch {
-    let mut sketch = ThetaSketchBuilder::default().build();
+    let mut sketch = ThetaSketchBuilder::default().build().unwrap();
     for i in 0..count {
         sketch.update(start + i);
     }
@@ -40,10 +40,10 @@ fn sketch_with_range(start: u64, count: u64) -> ThetaSketch 
{
 
 #[test]
 fn test_basic_difference() {
-    let mut a = ThetaSketchBuilder::default().build();
+    let mut a = ThetaSketchBuilder::default().build().unwrap();
     a.update("shared");
     a.update("only_a");
-    let mut b = ThetaSketchBuilder::default().build();
+    let mut b = ThetaSketchBuilder::default().build().unwrap();
     b.update("shared");
     b.update("only_b");
 
@@ -71,7 +71,7 @@ fn test_accepts_updatable_and_compact_inputs() {
 
 #[test]
 fn test_seed_mismatch_returns_error() {
-    let mut one_other_seed = ThetaSketchBuilder::default().seed(2).build();
+    let mut one_other_seed = 
ThetaSketchBuilder::default().seed(2).build().unwrap();
     one_other_seed.update("value");
     let good = sketch_with_range(0, 10);
 
@@ -89,7 +89,7 @@ fn test_seed_mismatch_returns_error() {
 #[test]
 fn test_seed_mismatch_ignored_for_empty_inputs() {
     // Empty inputs carry no keys, so their seeds are not validated.
-    let empty_other_seed = ThetaSketchBuilder::default().seed(2).build();
+    let empty_other_seed = 
ThetaSketchBuilder::default().seed(2).build().unwrap();
     let good = sketch_with_range(0, 10);
 
     let a_not_b = ThetaANotB::default();
@@ -103,7 +103,7 @@ fn test_seed_mismatch_ignored_for_empty_inputs() {
 
 #[test]
 fn test_empty_a_returns_empty() {
-    let empty = ThetaSketchBuilder::default().build();
+    let empty = ThetaSketchBuilder::default().build().unwrap();
     let b = sketch_with_range(0, 1000);
 
     let a_not_b = ThetaANotB::default();
@@ -117,7 +117,7 @@ fn test_empty_a_returns_empty() {
 #[test]
 fn test_empty_b_returns_a() {
     let a = sketch_with_range(0, 1000);
-    let empty = ThetaSketchBuilder::default().build();
+    let empty = ThetaSketchBuilder::default().build().unwrap();
 
     let a_not_b = ThetaANotB::default();
     let r = a_not_b.compute(&a, &empty, true).unwrap();
@@ -186,7 +186,7 @@ fn test_exact_superset_b_returns_empty() {
 #[test]
 fn test_result_ordering() {
     let a = sketch_with_range(0, 64);
-    let empty = ThetaSketchBuilder::default().build();
+    let empty = ThetaSketchBuilder::default().build().unwrap();
 
     let a_not_b = ThetaANotB::default();
 
diff --git a/tests-integration/tests/theta_test/intersection.rs 
b/tests-integration/tests/theta_test/intersection.rs
index 49c7593..dd11896 100644
--- a/tests-integration/tests/theta_test/intersection.rs
+++ b/tests-integration/tests/theta_test/intersection.rs
@@ -27,7 +27,7 @@ use googletest::prelude::near;
 use googletest::prelude::none;
 
 fn sketch_with_range(start: u64, count: u64) -> ThetaSketch {
-    let mut sketch = ThetaSketchBuilder::default().build();
+    let mut sketch = ThetaSketchBuilder::default().build().unwrap();
     for i in 0..count {
         sketch.update(start + i);
     }
@@ -36,7 +36,7 @@ fn sketch_with_range(start: u64, count: u64) -> ThetaSketch {
 
 #[test]
 fn test_has_result_state_machine() {
-    let mut a = ThetaSketchBuilder::default().build();
+    let mut a = ThetaSketchBuilder::default().build().unwrap();
     a.update("x");
 
     let mut i = ThetaIntersection::default();
@@ -54,11 +54,11 @@ fn test_result_before_first_update_returns_none() {
 
 #[test]
 fn test_update_accepts_compact_sketch() {
-    let mut a = ThetaSketchBuilder::default().build();
+    let mut a = ThetaSketchBuilder::default().build().unwrap();
     a.update("x");
     a.update("y");
 
-    let mut b = ThetaSketchBuilder::default().build();
+    let mut b = ThetaSketchBuilder::default().build().unwrap();
     b.update("y");
     b.update("z");
 
@@ -70,7 +70,7 @@ fn test_update_accepts_compact_sketch() {
     assert_eq!(r.estimate(), 1.0);
     assert!(r.is_ordered());
 
-    let mut c = ThetaSketchBuilder::default().build();
+    let mut c = ThetaSketchBuilder::default().build().unwrap();
     c.update("a");
     c.update("b");
     c.update("c");
@@ -84,7 +84,7 @@ fn test_update_accepts_compact_sketch() {
 
 #[test]
 fn test_seed_mismatch_behaviour_for_empty_sketch() {
-    let empty_other_seed = ThetaSketchBuilder::default().seed(2).build();
+    let empty_other_seed = 
ThetaSketchBuilder::default().seed(2).build().unwrap();
     let mut i = ThetaIntersection::with_seed(1);
 
     i.update(&empty_other_seed).unwrap();
@@ -95,7 +95,7 @@ fn test_seed_mismatch_behaviour_for_empty_sketch() {
 
 #[test]
 fn test_seed_mismatch_behaviour() {
-    let mut one_other_seed = ThetaSketchBuilder::default().seed(2).build();
+    let mut one_other_seed = 
ThetaSketchBuilder::default().seed(2).build().unwrap();
     one_other_seed.update("value");
     let mut i = ThetaIntersection::with_seed(1);
 
@@ -104,9 +104,9 @@ fn test_seed_mismatch_behaviour() {
 
 #[test]
 fn test_terminal_empty_state_ignores_future_updates() {
-    let empty = ThetaSketchBuilder::default().build();
+    let empty = ThetaSketchBuilder::default().build().unwrap();
 
-    let mut non_empty = ThetaSketchBuilder::default().build();
+    let mut non_empty = ThetaSketchBuilder::default().build().unwrap();
     non_empty.update("x");
 
     let mut i = ThetaIntersection::default();
@@ -119,7 +119,7 @@ fn test_terminal_empty_state_ignores_future_updates() {
 
 #[test]
 fn test_to_sketch_unordered_is_not_ordered() {
-    let mut a = ThetaSketchBuilder::default().build();
+    let mut a = ThetaSketchBuilder::default().build().unwrap();
     for i in 0..64 {
         a.update(i);
     }
@@ -132,7 +132,7 @@ fn test_to_sketch_unordered_is_not_ordered() {
 
 #[test]
 fn test_empty_update_twice() {
-    let empty = ThetaSketchBuilder::default().build();
+    let empty = ThetaSketchBuilder::default().build().unwrap();
     let mut i = ThetaIntersection::default();
 
     i.update(&empty).unwrap();
@@ -154,7 +154,8 @@ fn test_empty_update_twice() {
 fn test_non_empty_no_retained_keys() {
     let mut s = ThetaSketchBuilder::default()
         .sampling_probability(0.001)
-        .build();
+        .build()
+        .unwrap();
     s.update(1u64);
 
     let mut i = ThetaIntersection::default();
@@ -314,7 +315,7 @@ fn test_estimation_disjoint_ordered() {
 
 #[test]
 fn test_seed_mismatch_non_empty_returns_error() {
-    let mut s = ThetaSketchBuilder::default().build();
+    let mut s = ThetaSketchBuilder::default().build().unwrap();
     s.update(1u64);
 
     let mut i = ThetaIntersection::with_seed(123);
diff --git a/tests-integration/tests/theta_test/jaccard_similarity.rs 
b/tests-integration/tests/theta_test/jaccard_similarity.rs
index 104dd51..aa72912 100644
--- a/tests-integration/tests/theta_test/jaccard_similarity.rs
+++ b/tests-integration/tests/theta_test/jaccard_similarity.rs
@@ -41,7 +41,7 @@ fn assert_jaccard_estimate(actual: JaccardSimilarity, 
expected: f64) {
 }
 
 fn sketch_with_range(start: u64, count: u64) -> ThetaSketch {
-    let mut sketch = ThetaSketchBuilder::default().build();
+    let mut sketch = ThetaSketchBuilder::default().build().unwrap();
     for value in start..start + count {
         sketch.update(value);
     }
@@ -49,7 +49,7 @@ fn sketch_with_range(start: u64, count: u64) -> ThetaSketch {
 }
 
 fn sketch_with_range_and_seed(start: u64, count: u64, seed: u64) -> 
ThetaSketch {
-    let mut sketch = ThetaSketchBuilder::default().seed(seed).build();
+    let mut sketch = ThetaSketchBuilder::default().seed(seed).build().unwrap();
     for value in start..start + count {
         sketch.update(value);
     }
@@ -58,8 +58,8 @@ fn sketch_with_range_and_seed(start: u64, count: u64, seed: 
u64) -> ThetaSketch
 
 #[test]
 fn test_empty() {
-    let sketch_a = ThetaSketchBuilder::default().build();
-    let sketch_b = ThetaSketchBuilder::default().build();
+    let sketch_a = ThetaSketchBuilder::default().build().unwrap();
+    let sketch_b = ThetaSketchBuilder::default().build().unwrap();
 
     let operator = ThetaJaccardSimilarity::default();
     let jaccard = operator.compute(&sketch_a, &sketch_b).unwrap();
@@ -70,7 +70,7 @@ fn test_empty() {
 
 #[test]
 fn test_exactly_equal() {
-    let empty = ThetaSketchBuilder::default().build();
+    let empty = ThetaSketchBuilder::default().build().unwrap();
     let sketch_a = sketch_with_range(0, 1000);
     let sketch_b = sketch_with_range(0, 1000);
     let sketch_c = sketch_with_range(1000, 1000);
@@ -162,10 +162,10 @@ fn test_half_overlap_estimation_mode_custom_seed() {
 
 #[test]
 fn test_seed_mismatch() {
-    let empty = ThetaSketchBuilder::default().build();
-    let mut sketch_a = ThetaSketchBuilder::default().build();
+    let empty = ThetaSketchBuilder::default().build().unwrap();
+    let mut sketch_a = ThetaSketchBuilder::default().build().unwrap();
     sketch_a.update(1u64);
-    let mut sketch_b = ThetaSketchBuilder::default().seed(123).build();
+    let mut sketch_b = 
ThetaSketchBuilder::default().seed(123).build().unwrap();
     sketch_b.update(1u64);
 
     assert_that!(
@@ -187,13 +187,16 @@ fn test_seed_mismatch() {
 fn test_distinct_non_empty_sketches_with_no_retained_entries_are_uncertain() {
     let mut sketch_a = ThetaSketchBuilder::default()
         .sampling_probability(1e-12)
-        .build();
+        .build()
+        .unwrap();
     let mut sketch_b = ThetaSketchBuilder::default()
         .sampling_probability(1e-12)
-        .build();
+        .build()
+        .unwrap();
     let mut different_theta = ThetaSketchBuilder::default()
         .sampling_probability(2e-12)
-        .build();
+        .build()
+        .unwrap();
     sketch_a.update("apple");
     sketch_b.update("banana");
     different_theta.update("orange");
diff --git a/tests-integration/tests/theta_test/sketch.rs 
b/tests-integration/tests/theta_test/sketch.rs
index f4d976f..5bf7aaa 100644
--- a/tests-integration/tests/theta_test/sketch.rs
+++ b/tests-integration/tests/theta_test/sketch.rs
@@ -16,6 +16,7 @@
 // under the License.
 
 use datasketches::common::NumStdDev;
+use datasketches::error::ErrorKind;
 use datasketches::hash::value::canonical_float;
 use datasketches::theta::ThetaSketchBuilder;
 use googletest::assert_that;
@@ -25,9 +26,21 @@ use googletest::prelude::le;
 use googletest::prelude::lt;
 use googletest::prelude::near;
 
+#[test]
+fn builder_validates_configuration_at_build() {
+    let error = ThetaSketchBuilder::default().lg_k(4).build().unwrap_err();
+    assert_eq!(error.kind(), ErrorKind::InvalidArgument);
+
+    let error = ThetaSketchBuilder::default()
+        .sampling_probability(f32::NAN)
+        .build()
+        .unwrap_err();
+    assert_eq!(error.kind(), ErrorKind::InvalidArgument);
+}
+
 #[test]
 fn test_basic_update() {
-    let mut sketch = ThetaSketchBuilder::default().lg_k(12).build();
+    let mut sketch = ThetaSketchBuilder::default().lg_k(12).build().unwrap();
     assert!(sketch.is_empty());
     assert_eq!(sketch.estimate(), 0.0);
 
@@ -41,7 +54,7 @@ fn test_basic_update() {
 
 #[test]
 fn test_update_various_types() {
-    let mut sketch = ThetaSketchBuilder::default().lg_k(12).build();
+    let mut sketch = ThetaSketchBuilder::default().lg_k(12).build().unwrap();
 
     sketch.update("string");
     sketch.update(42i64);
@@ -56,7 +69,7 @@ fn test_update_various_types() {
     assert!(!sketch.is_empty());
     assert_eq!(sketch.estimate(), 5.0);
 
-    let mut sketch = ThetaSketchBuilder::default().lg_k(12).build();
+    let mut sketch = ThetaSketchBuilder::default().lg_k(12).build().unwrap();
 
     sketch.update("string");
     sketch.update(42i64);
@@ -74,7 +87,7 @@ fn test_update_various_types() {
 
 #[test]
 fn test_duplicate_updates() {
-    let mut sketch = ThetaSketchBuilder::default().lg_k(12).build();
+    let mut sketch = ThetaSketchBuilder::default().lg_k(12).build().unwrap();
 
     for _ in 0..100 {
         sketch.update("same_value");
@@ -85,7 +98,7 @@ fn test_duplicate_updates() {
 
 #[test]
 fn test_theta_reduction() {
-    let mut sketch = ThetaSketchBuilder::default().lg_k(5).build(); // Small k 
to trigger theta reduction
+    let mut sketch = ThetaSketchBuilder::default().lg_k(5).build().unwrap(); 
// Small k to trigger theta reduction
     assert!(!sketch.is_estimation_mode()); // Should be in estimation mode
 
     // Insert many values to trigger theta reduction
@@ -99,8 +112,8 @@ fn test_theta_reduction() {
 
 #[test]
 fn test_trim() {
-    let mut exact = ThetaSketchBuilder::default().lg_k(12).build();
-    let mut sketch = ThetaSketchBuilder::default().lg_k(5).build();
+    let mut exact = ThetaSketchBuilder::default().lg_k(12).build().unwrap();
+    let mut sketch = ThetaSketchBuilder::default().lg_k(5).build().unwrap();
 
     for i in 0..1000 {
         exact.update(i);
@@ -124,7 +137,7 @@ fn test_trim() {
 
 #[test]
 fn test_reset() {
-    let mut sketch = ThetaSketchBuilder::default().lg_k(5).build();
+    let mut sketch = ThetaSketchBuilder::default().lg_k(5).build().unwrap();
 
     // Insert many values
     for i in 0..1000 {
@@ -147,7 +160,7 @@ fn test_reset() {
 
 #[test]
 fn test_iterator() {
-    let mut sketch = ThetaSketchBuilder::default().lg_k(12).build();
+    let mut sketch = ThetaSketchBuilder::default().lg_k(12).build().unwrap();
 
     sketch.update("value1");
     sketch.update("value2");
@@ -159,7 +172,7 @@ fn test_iterator() {
 
 #[test]
 fn test_bounds_empty_sketch() {
-    let sketch = ThetaSketchBuilder::default().lg_k(12).build();
+    let sketch = ThetaSketchBuilder::default().lg_k(12).build().unwrap();
     assert!(sketch.is_empty());
     assert!(!sketch.is_estimation_mode());
     assert_eq!(sketch.theta(), 1.0);
@@ -174,7 +187,7 @@ fn test_bounds_empty_sketch() {
 
 #[test]
 fn test_bounds_exact_mode() {
-    let mut sketch = ThetaSketchBuilder::default().lg_k(12).build();
+    let mut sketch = ThetaSketchBuilder::default().lg_k(12).build().unwrap();
     for i in 0..2000 {
         sketch.update(i);
     }
@@ -188,7 +201,7 @@ fn test_bounds_exact_mode() {
 
 #[test]
 fn test_bounds_estimation_mode() {
-    let mut sketch = ThetaSketchBuilder::default().lg_k(12).build();
+    let mut sketch = ThetaSketchBuilder::default().lg_k(12).build().unwrap();
     let n = 10000;
     for i in 0..n {
         sketch.update(i);
@@ -228,7 +241,8 @@ fn test_bounds_with_sampling() {
     let mut sketch = ThetaSketchBuilder::default()
         .lg_k(12)
         .sampling_probability(0.5)
-        .build();
+        .build()
+        .unwrap();
 
     for i in 0..1000 {
         sketch.update(i);
@@ -248,7 +262,7 @@ fn test_bounds_with_sampling() {
 
 #[test]
 fn test_bounds_all_num_std_devs() {
-    let mut sketch = ThetaSketchBuilder::default().lg_k(12).build();
+    let mut sketch = ThetaSketchBuilder::default().lg_k(12).build().unwrap();
     for i in 0..10000 {
         sketch.update(i);
     }
@@ -275,7 +289,8 @@ fn test_bounds_empty_estimation_mode() {
     let sketch = ThetaSketchBuilder::default()
         .lg_k(12)
         .sampling_probability(0.1)
-        .build();
+        .build()
+        .unwrap();
 
     // The sketch is empty but theta < 1.0, so it's in estimation mode
     // However, when empty, both bounds should return 0.0 per Java 
implementation
@@ -293,7 +308,8 @@ fn 
test_compact_preserves_logical_non_empty_after_screened_update() {
             let mut sketch = ThetaSketchBuilder::default()
                 .lg_k(12)
                 .sampling_probability(0.5)
-                .build();
+                .build()
+                .unwrap();
             sketch.update(*candidate);
             !sketch.is_empty() && sketch.num_retained() == 0
         })
@@ -302,7 +318,8 @@ fn 
test_compact_preserves_logical_non_empty_after_screened_update() {
     let mut sketch = ThetaSketchBuilder::default()
         .lg_k(12)
         .sampling_probability(0.5)
-        .build();
+        .build()
+        .unwrap();
     sketch.update(screened_value);
 
     assert!(!sketch.is_empty());
diff --git a/tests-integration/tests/theta_test/union.rs 
b/tests-integration/tests/theta_test/union.rs
index c19eb9d..5b5ede5 100644
--- a/tests-integration/tests/theta_test/union.rs
+++ b/tests-integration/tests/theta_test/union.rs
@@ -15,6 +15,7 @@
 // specific language governing permissions and limitations
 // under the License.
 
+use datasketches::error::ErrorKind;
 use datasketches::theta::CompactThetaSketch;
 use datasketches::theta::ThetaSketch;
 use datasketches::theta::ThetaSketchBuilder;
@@ -25,8 +26,20 @@ use googletest::prelude::err;
 use googletest::prelude::le;
 use googletest::prelude::near;
 
+#[test]
+fn builder_validates_configuration_at_build() {
+    let error = ThetaUnionBuilder::default().lg_k(27).build().unwrap_err();
+    assert_eq!(error.kind(), ErrorKind::InvalidArgument);
+
+    let error = ThetaUnionBuilder::default()
+        .sampling_probability(0.0)
+        .build()
+        .unwrap_err();
+    assert_eq!(error.kind(), ErrorKind::InvalidArgument);
+}
+
 fn sketch_with_range(lg_k: u8, start: i64, count: i64) -> ThetaSketch {
-    let mut sketch = ThetaSketchBuilder::default().lg_k(lg_k).build();
+    let mut sketch = ThetaSketchBuilder::default().lg_k(lg_k).build().unwrap();
     for value in start..start + count {
         sketch.update(value);
     }
@@ -45,8 +58,8 @@ fn assert_estimate_close(sketch: &CompactThetaSketch, 
expected: f64, tolerance:
 
 #[test]
 fn test_empty_union() {
-    let sketch = ThetaSketchBuilder::default().build();
-    let mut union = ThetaUnionBuilder::default().build();
+    let sketch = ThetaSketchBuilder::default().build().unwrap();
+    let mut union = ThetaUnionBuilder::default().build().unwrap();
     let result = union.to_sketch(true);
     assert_eq!(result.num_retained(), 0);
     assert!(result.is_empty());
@@ -63,10 +76,11 @@ fn test_empty_union() {
 fn test_non_empty_no_retained_keys() {
     let mut sketch = ThetaSketchBuilder::default()
         .sampling_probability(0.001)
-        .build();
+        .build()
+        .unwrap();
     sketch.update(1u64);
 
-    let mut union = ThetaUnionBuilder::default().build();
+    let mut union = ThetaUnionBuilder::default().build().unwrap();
     union.update(&sketch).unwrap();
     let result = union.to_sketch(true);
     assert_eq!(result.num_retained(), 0);
@@ -77,17 +91,17 @@ fn test_non_empty_no_retained_keys() {
 
 #[test]
 fn test_exact_mode_half_overlap() {
-    let mut sketch1 = ThetaSketchBuilder::default().build();
+    let mut sketch1 = ThetaSketchBuilder::default().build().unwrap();
     for value in 0i64..1000i64 {
         sketch1.update(value);
     }
 
-    let mut sketch2 = ThetaSketchBuilder::default().build();
+    let mut sketch2 = ThetaSketchBuilder::default().build().unwrap();
     for value in 500i64..1500i64 {
         sketch2.update(value);
     }
 
-    let mut union = ThetaUnionBuilder::default().build();
+    let mut union = ThetaUnionBuilder::default().build().unwrap();
     union.update(&sketch1).unwrap();
     union.update(&sketch2).unwrap();
     let result = union.to_sketch(true);
@@ -104,19 +118,19 @@ fn test_exact_mode_half_overlap() {
 
 #[test]
 fn test_exact_mode_half_overlap_compact() {
-    let mut sketch1 = ThetaSketchBuilder::default().build();
+    let mut sketch1 = ThetaSketchBuilder::default().build().unwrap();
     for value in 0i64..1000i64 {
         sketch1.update(value);
     }
     let compact1 = 
CompactThetaSketch::deserialize(&sketch1.compact(true).serialize()).unwrap();
 
-    let mut sketch2 = ThetaSketchBuilder::default().build();
+    let mut sketch2 = ThetaSketchBuilder::default().build().unwrap();
     for value in 500i64..1500i64 {
         sketch2.update(value);
     }
     let compact2 = 
CompactThetaSketch::deserialize(&sketch2.compact(true).serialize()).unwrap();
 
-    let mut union = ThetaUnionBuilder::default().build();
+    let mut union = ThetaUnionBuilder::default().build().unwrap();
     union.update(&compact1).unwrap();
     union.update(&compact2).unwrap();
     let result = union.to_sketch(true);
@@ -127,17 +141,17 @@ fn test_exact_mode_half_overlap_compact() {
 
 #[test]
 fn test_estimation_mode_half_overlap() {
-    let mut sketch1 = ThetaSketchBuilder::default().build();
+    let mut sketch1 = ThetaSketchBuilder::default().build().unwrap();
     for value in 0i64..10000i64 {
         sketch1.update(value);
     }
 
-    let mut sketch2 = ThetaSketchBuilder::default().build();
+    let mut sketch2 = ThetaSketchBuilder::default().build().unwrap();
     for value in 5000i64..15000i64 {
         sketch2.update(value);
     }
 
-    let mut union = ThetaUnionBuilder::default().build();
+    let mut union = ThetaUnionBuilder::default().build().unwrap();
     union.update(&sketch1).unwrap();
     union.update(&sketch2).unwrap();
     let result = union.to_sketch(true);
@@ -154,38 +168,38 @@ fn test_estimation_mode_half_overlap() {
 
 #[test]
 fn test_seed_mismatch() {
-    let mut sketch = ThetaSketchBuilder::default().build();
+    let mut sketch = ThetaSketchBuilder::default().build().unwrap();
     sketch.update(1u64);
 
-    let mut union = ThetaUnionBuilder::default().seed(123).build();
+    let mut union = ThetaUnionBuilder::default().seed(123).build().unwrap();
     assert_that!(union.update(&sketch), err(anything()));
 }
 
 #[test]
 fn test_larger_k() {
-    let mut sketch1 = ThetaSketchBuilder::default().lg_k(14).build();
+    let mut sketch1 = ThetaSketchBuilder::default().lg_k(14).build().unwrap();
     for value in 0i64..16384i64 {
         sketch1.update(value);
     }
 
-    let mut sketch2 = ThetaSketchBuilder::default().lg_k(14).build();
+    let mut sketch2 = ThetaSketchBuilder::default().lg_k(14).build().unwrap();
     for value in 0i64..26384i64 {
         sketch2.update(value);
     }
 
-    let mut sketch3 = ThetaSketchBuilder::default().lg_k(14).build();
+    let mut sketch3 = ThetaSketchBuilder::default().lg_k(14).build().unwrap();
     for value in 0i64..86384i64 {
         sketch3.update(value);
     }
 
-    let mut union1 = ThetaUnionBuilder::default().lg_k(16).build();
+    let mut union1 = ThetaUnionBuilder::default().lg_k(16).build().unwrap();
     union1.update(&sketch2).unwrap();
     union1.update(&sketch1).unwrap();
     union1.update(&sketch3).unwrap();
     let result1 = union1.to_sketch(true);
     assert_eq!(result1.estimate(), sketch3.estimate());
 
-    let mut union2 = ThetaUnionBuilder::default().lg_k(16).build();
+    let mut union2 = ThetaUnionBuilder::default().lg_k(16).build().unwrap();
     union2.update(&sketch1).unwrap();
     union2.update(&sketch3).unwrap();
     union2.update(&sketch2).unwrap();
@@ -200,7 +214,7 @@ fn test_exact_union_no_overlap() {
     let sketch1 = sketch_with_range(lg_k, 0, k / 2);
     let sketch2 = sketch_with_range(lg_k, k / 2, k / 2);
 
-    let mut union = ThetaUnionBuilder::default().lg_k(lg_k).build();
+    let mut union = ThetaUnionBuilder::default().lg_k(lg_k).build().unwrap();
     union.update(&sketch1).unwrap();
     union.update(&sketch2).unwrap();
 
@@ -217,7 +231,7 @@ fn test_estimation_union_no_overlap() {
     let sketch1 = sketch_with_range(lg_k, 0, 2 * k);
     let sketch2 = sketch_with_range(lg_k, 2 * k, 2 * k);
 
-    let mut union = ThetaUnionBuilder::default().lg_k(lg_k).build();
+    let mut union = ThetaUnionBuilder::default().lg_k(lg_k).build().unwrap();
     union.update(&sketch1).unwrap();
     union.update(&sketch2).unwrap();
 
@@ -235,7 +249,7 @@ fn test_exact_union_with_overlap() {
     let sketch1 = sketch_with_range(lg_k, 0, k / 2);
     let sketch2 = sketch_with_range(lg_k, 0, k);
 
-    let mut union = ThetaUnionBuilder::default().lg_k(lg_k).build();
+    let mut union = ThetaUnionBuilder::default().lg_k(lg_k).build().unwrap();
     union.update(&sketch1).unwrap();
     union.update(&sketch2).unwrap();
 
@@ -254,11 +268,11 @@ fn test_ordered_and_unordered_compact_inputs() {
     let compact_ordered = sketch2.compact(true);
     let compact_unordered = sketch2.compact(false);
 
-    let mut ordered_union = ThetaUnionBuilder::default().lg_k(lg_k).build();
+    let mut ordered_union = 
ThetaUnionBuilder::default().lg_k(lg_k).build().unwrap();
     ordered_union.update(&sketch1).unwrap();
     ordered_union.update(&compact_ordered).unwrap();
 
-    let mut unordered_union = ThetaUnionBuilder::default().lg_k(lg_k).build();
+    let mut unordered_union = 
ThetaUnionBuilder::default().lg_k(lg_k).build().unwrap();
     unordered_union.update(&sketch1).unwrap();
     unordered_union.update(&compact_unordered).unwrap();
 
@@ -278,7 +292,7 @@ fn test_result_ordering_forms_have_same_estimate() {
     let sketch1 = sketch_with_range(12, 0, 8192);
     let sketch2 = sketch_with_range(12, 8192, 1024);
 
-    let mut union = ThetaUnionBuilder::default().lg_k(12).build();
+    let mut union = ThetaUnionBuilder::default().lg_k(12).build().unwrap();
     union.update(&sketch1).unwrap();
     union.update(&sketch2).unwrap();
 
@@ -299,7 +313,7 @@ fn test_multi_union() {
         (126_797, 26_797),
         (153_594, 26_797),
     ];
-    let mut union = ThetaUnionBuilder::default().lg_k(lg_k).build();
+    let mut union = ThetaUnionBuilder::default().lg_k(lg_k).build().unwrap();
 
     for (start, count) in ranges {
         let sketch = sketch_with_range(lg_k, start, count);
@@ -316,7 +330,7 @@ fn test_result_does_not_reset_union() {
     let compact1 = sketch_with_range(lg_k, 0, k).compact(true);
     let compact2 = sketch_with_range(lg_k, k, k).compact(true);
 
-    let mut union = ThetaUnionBuilder::default().lg_k(lg_k).build();
+    let mut union = ThetaUnionBuilder::default().lg_k(lg_k).build().unwrap();
     union.update(&compact1).unwrap();
     union.update(&compact2).unwrap();
     let first = union.to_sketch(true);
@@ -333,7 +347,7 @@ fn test_union_full_overlap() {
     let compact1 = sketch_with_range(lg_k, 0, k).compact(true);
     let compact2 = sketch_with_range(lg_k, 0, k).compact(true);
 
-    let mut union = ThetaUnionBuilder::default().lg_k(lg_k).build();
+    let mut union = ThetaUnionBuilder::default().lg_k(lg_k).build().unwrap();
     union.update(&compact1).unwrap();
     union.update(&compact2).unwrap();
     let result = union.to_sketch(true);
@@ -356,11 +370,11 @@ fn 
test_ordered_input_early_stop_matches_unordered_input() {
         let unordered1 = sketch1.compact(false);
         let unordered2 = sketch2.compact(false);
 
-        let mut ordered_union = ThetaUnionBuilder::default().lg_k(lg_k + 
1).build();
+        let mut ordered_union = ThetaUnionBuilder::default().lg_k(lg_k + 
1).build().unwrap();
         ordered_union.update(&ordered1).unwrap();
         ordered_union.update(&ordered2).unwrap();
 
-        let mut unordered_union = ThetaUnionBuilder::default().lg_k(lg_k + 
1).build();
+        let mut unordered_union = ThetaUnionBuilder::default().lg_k(lg_k + 
1).build().unwrap();
         unordered_union.update(&unordered1).unwrap();
         unordered_union.update(&unordered2).unwrap();
 
@@ -378,7 +392,7 @@ fn test_union_cutback_to_k() {
     let compact1 = sketch_with_range(lg_k, 0, 3 * k).compact(true);
     let compact2 = sketch_with_range(lg_k, 6 * k, 3 * k).compact(true);
 
-    let mut union = ThetaUnionBuilder::default().lg_k(lg_k).build();
+    let mut union = ThetaUnionBuilder::default().lg_k(lg_k).build().unwrap();
     union.update(&compact1).unwrap();
     union.update(&compact2).unwrap();
     let result = union.to_sketch(true);
@@ -389,23 +403,23 @@ fn test_union_cutback_to_k() {
 
 #[test]
 fn test_union_empty_valid_rules() {
-    let empty1 = ThetaSketchBuilder::default().build().compact(true);
-    let empty2 = ThetaSketchBuilder::default().build().compact(true);
-    let mut one = ThetaSketchBuilder::default().build();
+    let empty1 = ThetaSketchBuilder::default().build().unwrap().compact(true);
+    let empty2 = ThetaSketchBuilder::default().build().unwrap().compact(true);
+    let mut one = ThetaSketchBuilder::default().build().unwrap();
     one.update(1i64);
     let one = one.compact(true);
 
-    let mut empty_union = ThetaUnionBuilder::default().lg_k(5).build();
+    let mut empty_union = 
ThetaUnionBuilder::default().lg_k(5).build().unwrap();
     empty_union.update(&empty1).unwrap();
     empty_union.update(&empty2).unwrap();
     assert!(empty_union.to_sketch(true).is_empty());
 
-    let mut left_non_empty_union = 
ThetaUnionBuilder::default().lg_k(5).build();
+    let mut left_non_empty_union = 
ThetaUnionBuilder::default().lg_k(5).build().unwrap();
     left_non_empty_union.update(&one).unwrap();
     left_non_empty_union.update(&empty2).unwrap();
     assert!(!left_non_empty_union.to_sketch(true).is_empty());
 
-    let mut right_non_empty_union = 
ThetaUnionBuilder::default().lg_k(5).build();
+    let mut right_non_empty_union = 
ThetaUnionBuilder::default().lg_k(5).build().unwrap();
     right_non_empty_union.update(&empty1).unwrap();
     right_non_empty_union.update(&one).unwrap();
     assert!(!right_non_empty_union.to_sketch(true).is_empty());
@@ -416,7 +430,7 @@ fn test_trim_to_k() {
     let hi_sketch = sketch_with_range(10, 0, 3749);
     let lo_sketch = sketch_with_range(9, 10_000, 1783);
 
-    let mut union = ThetaUnionBuilder::default().lg_k(10).build();
+    let mut union = ThetaUnionBuilder::default().lg_k(10).build().unwrap();
     union.update(&hi_sketch).unwrap();
     union.update(&lo_sketch).unwrap();
     let result = union.to_sketch(true);
@@ -427,7 +441,7 @@ fn test_trim_to_k() {
 #[test]
 fn test_builder_lg_k() {
     let sketch = sketch_with_range(10, 0, 1000);
-    let mut union = ThetaUnionBuilder::default().lg_k(10).build();
+    let mut union = ThetaUnionBuilder::default().lg_k(10).build().unwrap();
     union.update(&sketch).unwrap();
 
     assert_eq!(union.to_sketch(true).estimate(), 1000.0);
@@ -444,9 +458,9 @@ enum CornerSketchState {
 fn corner_sketch(state: CornerSketchState, p: f32, value: i64) -> ThetaSketch {
     let builder = ThetaSketchBuilder::default().lg_k(5);
     let mut sketch = match state {
-        CornerSketchState::Empty | CornerSketchState::Exact => builder.build(),
+        CornerSketchState::Empty | CornerSketchState::Exact => 
builder.build().unwrap(),
         CornerSketchState::Estimation | CornerSketchState::Degenerate => {
-            builder.sampling_probability(p).build()
+            builder.sampling_probability(p).build().unwrap()
         }
     };
     if !matches!(state, CornerSketchState::Empty) {
@@ -657,7 +671,7 @@ fn test_corner_case_union_states() {
         let sketch_a = corner_sketch(state_a, p_a, value_a);
         let sketch_b = corner_sketch(state_b, p_b, value_b);
 
-        let mut union = ThetaUnionBuilder::default().build();
+        let mut union = ThetaUnionBuilder::default().build().unwrap();
         union.update(&sketch_a).unwrap();
         union.update(&sketch_b).unwrap();
         let result = union.to_sketch(true);
@@ -680,7 +694,7 @@ fn test_corner_case_union_states() {
 
         let compact_a = sketch_a.compact(true);
         let compact_b = sketch_b.compact(true);
-        let mut union = ThetaUnionBuilder::default().build();
+        let mut union = ThetaUnionBuilder::default().build().unwrap();
         union.update(&compact_a).unwrap();
         union.update(&compact_b).unwrap();
         let compact_result = union.to_sketch(true);
@@ -693,7 +707,7 @@ fn test_corner_case_union_states() {
 
 #[test]
 fn test_union_estimated_size() {
-    let mut union = ThetaUnionBuilder::default().build();
+    let mut union = ThetaUnionBuilder::default().build().unwrap();
     assert_eq!(union.estimated_size(), 1096);
 
     let sketch = sketch_with_range(12, 0, 1000);
diff --git a/tests-integration/tests/tuple_test/a_not_b.rs 
b/tests-integration/tests/tuple_test/a_not_b.rs
index cae015b..f766136 100644
--- a/tests-integration/tests/tuple_test/a_not_b.rs
+++ b/tests-integration/tests/tuple_test/a_not_b.rs
@@ -38,10 +38,10 @@ fn sorted_entries(sketch: &CompactTupleSketch<u64>) -> 
Vec<(u64, u64)> {
 
 #[test]
 fn difference_keeps_only_a_summaries() {
-    let mut a = default_tuple_sketch_builder().build();
+    let mut a = default_tuple_sketch_builder().build().unwrap();
     a.update("shared", 3u64);
     a.update("only_a", 5u64);
-    let mut b = default_tuple_sketch_builder().build();
+    let mut b = default_tuple_sketch_builder().build().unwrap();
     b.update("shared", 9u64);
     b.update("only_b", 7u64);
 
@@ -72,8 +72,8 @@ fn accepts_mutable_and_compact_inputs() {
 
 #[test]
 fn input_and_result_ordering_preserve_entries() {
-    let mut a = default_tuple_sketch_builder().lg_k(8).build();
-    let mut b = default_tuple_sketch_builder().lg_k(8).build();
+    let mut a = default_tuple_sketch_builder().lg_k(8).build().unwrap();
+    let mut b = default_tuple_sketch_builder().lg_k(8).build().unwrap();
     for value in 0..20_000 {
         a.update(value, 1u64);
     }
@@ -108,7 +108,7 @@ fn input_and_result_ordering_preserve_entries() {
 
 #[test]
 fn empty_inputs_do_not_impose_a_seed() {
-    let empty_other_seed = default_tuple_sketch_builder().seed(2).build();
+    let empty_other_seed = 
default_tuple_sketch_builder().seed(2).build().unwrap();
     let non_empty = tuple_sketch_with_range(0, 10);
     let op = TupleANotB::default();
 
@@ -127,9 +127,9 @@ fn empty_inputs_do_not_impose_a_seed() {
 
 #[test]
 fn non_empty_inputs_require_the_operator_seed() {
-    let mut other_seed = default_tuple_sketch_builder().seed(2).build();
+    let mut other_seed = 
default_tuple_sketch_builder().seed(2).build().unwrap();
     other_seed.update("value", 1u64);
-    let empty = default_tuple_sketch_builder().build();
+    let empty = default_tuple_sketch_builder().build().unwrap();
     let good = tuple_sketch_with_range(0, 10);
     let op = TupleANotB::default();
 
@@ -146,7 +146,8 @@ fn 
empty_b_preserves_logically_non_empty_a_without_retained_entries() {
         .find(|candidate| {
             let mut sketch = default_tuple_sketch_builder()
                 .sampling_probability(0.001)
-                .build();
+                .build()
+                .unwrap();
             sketch.update(*candidate, 1u64);
             !sketch.is_empty() && sketch.num_retained() == 0
         })
@@ -154,9 +155,10 @@ fn 
empty_b_preserves_logically_non_empty_a_without_retained_entries() {
 
     let mut a = default_tuple_sketch_builder()
         .sampling_probability(0.001)
-        .build();
+        .build()
+        .unwrap();
     a.update(screened_value, 1u64);
-    let empty_b = default_tuple_sketch_builder().seed(999).build();
+    let empty_b = default_tuple_sketch_builder().seed(999).build().unwrap();
 
     let result = TupleANotB::default().compute(&a, &empty_b, true).unwrap();
 
@@ -167,8 +169,8 @@ fn 
empty_b_preserves_logically_non_empty_a_without_retained_entries() {
 
 #[test]
 fn estimation_bounds_cover_the_true_difference() {
-    let mut a = default_tuple_sketch_builder().lg_k(8).build();
-    let mut b = default_tuple_sketch_builder().lg_k(8).build();
+    let mut a = default_tuple_sketch_builder().lg_k(8).build().unwrap();
+    let mut b = default_tuple_sketch_builder().lg_k(8).build().unwrap();
     for value in 0..50_000 {
         a.update(value, 1u64);
     }
diff --git a/tests-integration/tests/tuple_test/intersection.rs 
b/tests-integration/tests/tuple_test/intersection.rs
index 8983c06..456058b 100644
--- a/tests-integration/tests/tuple_test/intersection.rs
+++ b/tests-integration/tests/tuple_test/intersection.rs
@@ -64,10 +64,10 @@ fn result_before_first_update_returns_none() {
 
 #[test]
 fn overlap_combines_summaries() {
-    let mut a = default_tuple_sketch_builder().build();
+    let mut a = default_tuple_sketch_builder().build().unwrap();
     a.update("shared", 3u64);
     a.update("only_a", 100u64);
-    let mut b = default_tuple_sketch_builder().build();
+    let mut b = default_tuple_sketch_builder().build().unwrap();
     b.update("shared", 4u64);
     b.update("only_b", 200u64);
 
@@ -114,7 +114,8 @@ fn 
logically_non_empty_input_without_retained_entries_is_preserved() {
         .find(|candidate| {
             let mut sketch = default_tuple_sketch_builder()
                 .sampling_probability(0.001)
-                .build();
+                .build()
+                .unwrap();
             sketch.update(*candidate, 1u64);
             !sketch.is_empty() && sketch.num_retained() == 0
         })
@@ -122,7 +123,8 @@ fn 
logically_non_empty_input_without_retained_entries_is_preserved() {
 
     let mut sketch = default_tuple_sketch_builder()
         .sampling_probability(0.001)
-        .build();
+        .build()
+        .unwrap();
     sketch.update(screened_value, 1u64);
 
     let mut intersection = TupleIntersection::new(SumPolicy);
@@ -136,8 +138,8 @@ fn 
logically_non_empty_input_without_retained_entries_is_preserved() {
 
 #[test]
 fn only_non_empty_inputs_require_the_operator_seed() {
-    let empty_other_seed = default_tuple_sketch_builder().seed(2).build();
-    let mut non_empty_other_seed = 
default_tuple_sketch_builder().seed(2).build();
+    let empty_other_seed = 
default_tuple_sketch_builder().seed(2).build().unwrap();
+    let mut non_empty_other_seed = 
default_tuple_sketch_builder().seed(2).build().unwrap();
     non_empty_other_seed.update("value", 1u64);
 
     let mut intersection = TupleIntersection::with_seed(SumPolicy, 1);
@@ -160,8 +162,8 @@ fn result_ordering_follows_the_request() {
 
 #[test]
 fn estimation_bounds_cover_the_true_intersection() {
-    let mut a = default_tuple_sketch_builder().lg_k(8).build();
-    let mut b = default_tuple_sketch_builder().lg_k(8).build();
+    let mut a = default_tuple_sketch_builder().lg_k(8).build().unwrap();
+    let mut b = default_tuple_sketch_builder().lg_k(8).build().unwrap();
     for value in 0..50_000 {
         a.update(value, 1u64);
     }
diff --git a/tests-integration/tests/tuple_test/jaccard_similarity.rs 
b/tests-integration/tests/tuple_test/jaccard_similarity.rs
index 7bb6c2a..0c54c2e 100644
--- a/tests-integration/tests/tuple_test/jaccard_similarity.rs
+++ b/tests-integration/tests/tuple_test/jaccard_similarity.rs
@@ -45,8 +45,8 @@ fn assert_jaccard_estimate(actual: JaccardSimilarity, 
expected: f64) {
 
 #[test]
 fn test_empty() {
-    let sketch_a = default_tuple_sketch_builder().build();
-    let sketch_b = default_tuple_sketch_builder().build();
+    let sketch_a = default_tuple_sketch_builder().build().unwrap();
+    let sketch_b = default_tuple_sketch_builder().build().unwrap();
 
     let operator = TupleJaccardSimilarity::default();
     let jaccard = operator.compute(&sketch_a, &sketch_b).unwrap();
@@ -57,8 +57,12 @@ fn test_empty() {
 
 #[test]
 fn test_summary_values_and_types_do_not_affect_similarity() {
-    let mut sketch_a = 
TupleSketchBuilder::new(DefaultUpdatePolicy::<u64>::default()).build();
-    let mut sketch_b = 
TupleSketchBuilder::new(DefaultUpdatePolicy::<i64>::default()).build();
+    let mut sketch_a = 
TupleSketchBuilder::new(DefaultUpdatePolicy::<u64>::default())
+        .build()
+        .unwrap();
+    let mut sketch_b = 
TupleSketchBuilder::new(DefaultUpdatePolicy::<i64>::default())
+        .build()
+        .unwrap();
     for key in 0..1000 {
         sketch_a.update(key, 1u64);
         sketch_b.update(key, -7i64);
@@ -96,13 +100,15 @@ fn test_half_overlap_estimation_mode() {
 #[test]
 fn test_custom_seed_and_seed_mismatch() {
     let seed = 123;
-    let empty = default_tuple_sketch_builder().build();
+    let empty = default_tuple_sketch_builder().build().unwrap();
     let mut sketch_a = 
TupleSketchBuilder::new(DefaultUpdatePolicy::<u64>::default())
         .seed(seed)
-        .build();
+        .build()
+        .unwrap();
     let mut sketch_b = 
TupleSketchBuilder::new(DefaultUpdatePolicy::<u64>::default())
         .seed(seed)
-        .build();
+        .build()
+        .unwrap();
     for value in 0..1000 {
         sketch_a.update(value, 1u64);
         sketch_b.update(value, 2u64);
@@ -131,10 +137,12 @@ fn test_custom_seed_and_seed_mismatch() {
 fn test_distinct_non_empty_sketches_with_no_retained_entries_are_uncertain() {
     let mut sketch_a = default_tuple_sketch_builder()
         .sampling_probability(1e-12)
-        .build();
+        .build()
+        .unwrap();
     let mut sketch_b = default_tuple_sketch_builder()
         .sampling_probability(1e-12)
-        .build();
+        .build()
+        .unwrap();
     sketch_a.update("apple", 1u64);
     sketch_b.update("banana", 1u64);
 
diff --git a/tests-integration/tests/tuple_test/main.rs 
b/tests-integration/tests/tuple_test/main.rs
index cbb6290..240e413 100644
--- a/tests-integration/tests/tuple_test/main.rs
+++ b/tests-integration/tests/tuple_test/main.rs
@@ -30,7 +30,7 @@ fn default_tuple_sketch_builder() -> 
TupleSketchBuilder<DefaultUpdatePolicy<u64>
 }
 
 fn tuple_sketch_with_range(start: u64, count: u64) -> 
TupleSketch<DefaultUpdatePolicy<u64>> {
-    let mut sketch = default_tuple_sketch_builder().build();
+    let mut sketch = default_tuple_sketch_builder().build().unwrap();
     for i in 0..count {
         sketch.update(start + i, 1u64);
     }
diff --git a/tests-integration/tests/tuple_test/sketch.rs 
b/tests-integration/tests/tuple_test/sketch.rs
index 9a17a9d..4298228 100644
--- a/tests-integration/tests/tuple_test/sketch.rs
+++ b/tests-integration/tests/tuple_test/sketch.rs
@@ -16,6 +16,7 @@
 // under the License.
 
 use datasketches::common::NumStdDev;
+use datasketches::error::ErrorKind;
 use datasketches::hash::value;
 use datasketches::tuple::CompactTupleSketch;
 use datasketches::tuple::DefaultUpdatePolicy;
@@ -30,9 +31,21 @@ use googletest::prelude::lt;
 
 use crate::default_tuple_sketch_builder;
 
+#[test]
+fn builder_validates_configuration_at_build() {
+    let error = default_tuple_sketch_builder().lg_k(4).build().unwrap_err();
+    assert_eq!(error.kind(), ErrorKind::InvalidArgument);
+
+    let error = default_tuple_sketch_builder()
+        .sampling_probability(f32::INFINITY)
+        .build()
+        .unwrap_err();
+    assert_eq!(error.kind(), ErrorKind::InvalidArgument);
+}
+
 #[test]
 fn updates_distinct_keys_and_accumulates_summaries() {
-    let mut sketch = default_tuple_sketch_builder().build();
+    let mut sketch = default_tuple_sketch_builder().build().unwrap();
     sketch.update("shared", 2u64);
     sketch.update("shared", 3u64);
     sketch.update("other", 7u64);
@@ -47,7 +60,7 @@ fn updates_distinct_keys_and_accumulates_summaries() {
 
 #[test]
 fn accepts_supported_hash_representations() {
-    let mut sketch = default_tuple_sketch_builder().build();
+    let mut sketch = default_tuple_sketch_builder().build().unwrap();
     sketch.update("string", 1u64);
     sketch.update(42i64, 1u64);
     sketch.update(42u64, 1u64);
@@ -60,7 +73,9 @@ fn accepts_supported_hash_representations() {
 
 #[test]
 fn default_update_policy_accepts_distinct_rhs_type() {
-    let mut sketch = 
TupleSketchBuilder::new(DefaultUpdatePolicy::<String>::default()).build();
+    let mut sketch = 
TupleSketchBuilder::new(DefaultUpdatePolicy::<String>::default())
+        .build()
+        .unwrap();
     sketch.update("key", "hello");
     sketch.update("key", " world");
 
@@ -94,7 +109,9 @@ where
 
 #[test]
 fn custom_update_policy_accepts_multiple_value_representations() {
-    let mut sketch = TupleSketchBuilder::new(ArraySumPolicy { num_values: 2 
}).build();
+    let mut sketch = TupleSketchBuilder::new(ArraySumPolicy { num_values: 2 })
+        .build()
+        .unwrap();
     sketch.update("key", &[1.0, 2.0]);
     sketch.update("key", vec![3.0, 4.0]);
 
@@ -104,7 +121,7 @@ fn 
custom_update_policy_accepts_multiple_value_representations() {
 
 #[test]
 fn trim_and_reset_update_public_state() {
-    let mut sketch = default_tuple_sketch_builder().lg_k(5).build();
+    let mut sketch = default_tuple_sketch_builder().lg_k(5).build().unwrap();
     for value in 0..1000 {
         sketch.update(value, 1u64);
     }
@@ -123,14 +140,14 @@ fn trim_and_reset_update_public_state() {
 
 #[test]
 fn bounds_cover_exact_and_estimation_results() {
-    let mut exact = default_tuple_sketch_builder().build();
+    let mut exact = default_tuple_sketch_builder().build().unwrap();
     for value in 0..100 {
         exact.update(value, 1u64);
     }
     assert_eq!(exact.lower_bound(NumStdDev::One), 100.0);
     assert_eq!(exact.upper_bound(NumStdDev::Three), 100.0);
 
-    let mut estimated = default_tuple_sketch_builder().lg_k(8).build();
+    let mut estimated = 
default_tuple_sketch_builder().lg_k(8).build().unwrap();
     for value in 0..50_000 {
         estimated.update(value, 1u64);
     }
@@ -151,7 +168,8 @@ fn bounds_cover_exact_and_estimation_results() {
 fn empty_sampled_sketch_has_zero_bounds() {
     let sketch = default_tuple_sketch_builder()
         .sampling_probability(0.1)
-        .build();
+        .build()
+        .unwrap();
 
     assert!(sketch.is_empty());
     assert!(sketch.is_estimation_mode());
@@ -185,7 +203,7 @@ fn assert_compact_preserves_state(
 #[test]
 fn compact_preserves_state_in_exact_and_estimation_modes() {
     for (lg_k, num_updates, expected_estimation_mode) in [(12, 2_000, false), 
(5, 5_000, true)] {
-        let mut sketch = default_tuple_sketch_builder().lg_k(lg_k).build();
+        let mut sketch = 
default_tuple_sketch_builder().lg_k(lg_k).build().unwrap();
         for key in 0..num_updates {
             sketch.update(key, key + 1);
             sketch.update(key, 10u64);
@@ -205,7 +223,8 @@ fn 
compact_preserves_logical_non_empty_after_screened_update() {
         .find(|candidate| {
             let mut sketch = default_tuple_sketch_builder()
                 .sampling_probability(0.5)
-                .build();
+                .build()
+                .unwrap();
             sketch.update(*candidate, 1u64);
             !sketch.is_empty() && sketch.num_retained() == 0
         })
@@ -213,7 +232,8 @@ fn 
compact_preserves_logical_non_empty_after_screened_update() {
 
     let mut sketch = default_tuple_sketch_builder()
         .sampling_probability(0.5)
-        .build();
+        .build()
+        .unwrap();
     sketch.update(screened_value, 1u64);
     let compact = sketch.compact(false);
 
diff --git a/tests-integration/tests/tuple_test/union.rs 
b/tests-integration/tests/tuple_test/union.rs
index b521888..87d0b8d 100644
--- a/tests-integration/tests/tuple_test/union.rs
+++ b/tests-integration/tests/tuple_test/union.rs
@@ -33,16 +33,28 @@ fn default_union_builder() -> 
TupleUnionBuilder<DefaultUnionPolicy<u64>> {
     TupleUnionBuilder::new(DefaultUnionPolicy::<u64>::default())
 }
 
+#[test]
+fn builder_validates_configuration_at_build() {
+    let error = default_union_builder().lg_k(27).build().unwrap_err();
+    assert_eq!(error.kind(), ErrorKind::InvalidArgument);
+
+    let error = default_union_builder()
+        .sampling_probability(-0.1)
+        .build()
+        .unwrap_err();
+    assert_eq!(error.kind(), ErrorKind::InvalidArgument);
+}
+
 #[test]
 fn union_combines_overlapping_summaries() {
-    let mut a = default_tuple_sketch_builder().build();
+    let mut a = default_tuple_sketch_builder().build().unwrap();
     a.update("shared", 3u64);
     a.update("only_a", 1u64);
-    let mut b = default_tuple_sketch_builder().build();
+    let mut b = default_tuple_sketch_builder().build().unwrap();
     b.update("shared", 4u64);
     b.update("only_b", 1u64);
 
-    let mut union = default_union_builder().build();
+    let mut union = default_union_builder().build().unwrap();
     union.update(&a).unwrap();
     union.update(&b).unwrap();
     let result = union.to_sketch(true);
@@ -58,7 +70,7 @@ fn accepts_mutable_and_compact_inputs() {
     let a = tuple_sketch_with_range(0, 500);
     let b = tuple_sketch_with_range(250, 500);
 
-    let mut union = default_union_builder().build();
+    let mut union = default_union_builder().build().unwrap();
     union.update(&a).unwrap();
     union.update(&b.compact(true)).unwrap();
 
@@ -68,7 +80,7 @@ fn accepts_mutable_and_compact_inputs() {
 #[test]
 fn reset_restores_the_initial_empty_state() {
     let input = tuple_sketch_with_range(0, 100);
-    let mut union = default_union_builder().build();
+    let mut union = default_union_builder().build().unwrap();
 
     assert!(union.to_sketch(true).is_empty());
     union.update(&input).unwrap();
@@ -82,9 +94,9 @@ fn reset_restores_the_initial_empty_state() {
 
 #[test]
 fn non_empty_input_requires_the_union_seed() {
-    let mut input = default_tuple_sketch_builder().seed(1).build();
+    let mut input = default_tuple_sketch_builder().seed(1).build().unwrap();
     input.update("value", 1u64);
-    let mut union = default_union_builder().seed(2).build();
+    let mut union = default_union_builder().seed(2).build().unwrap();
 
     let err = union.update(&input).unwrap_err();
     assert_eq!(err.kind(), ErrorKind::InvalidArgument);
@@ -109,12 +121,12 @@ impl SummaryCombinePolicy for MaxPolicy {
 
 #[test]
 fn custom_combine_policy_controls_overlapping_summaries() {
-    let mut a = default_tuple_sketch_builder().build();
+    let mut a = default_tuple_sketch_builder().build().unwrap();
     a.update("shared", 3u64);
-    let mut b = default_tuple_sketch_builder().build();
+    let mut b = default_tuple_sketch_builder().build().unwrap();
     b.update("shared", 9u64);
 
-    let mut union = TupleUnionBuilder::new(MaxPolicy).build();
+    let mut union = TupleUnionBuilder::new(MaxPolicy).build().unwrap();
     union.update(&a).unwrap();
     union.update(&b).unwrap();
 
@@ -124,7 +136,7 @@ fn custom_combine_policy_controls_overlapping_summaries() {
 #[test]
 fn result_ordering_follows_the_request() {
     let input = tuple_sketch_with_range(0, 100);
-    let mut union = default_union_builder().build();
+    let mut union = default_union_builder().build().unwrap();
     union.update(&input).unwrap();
 
     assert!(union.to_sketch(true).is_ordered());
@@ -133,8 +145,8 @@ fn result_ordering_follows_the_request() {
 
 #[test]
 fn estimation_bounds_cover_the_true_union() {
-    let mut a = default_tuple_sketch_builder().lg_k(8).build();
-    let mut b = default_tuple_sketch_builder().lg_k(8).build();
+    let mut a = default_tuple_sketch_builder().lg_k(8).build().unwrap();
+    let mut b = default_tuple_sketch_builder().lg_k(8).build().unwrap();
     for value in 0..50_000 {
         a.update(value, 1u64);
     }
@@ -142,7 +154,7 @@ fn estimation_bounds_cover_the_true_union() {
         b.update(value, 1u64);
     }
 
-    let mut union = default_union_builder().lg_k(8).build();
+    let mut union = default_union_builder().lg_k(8).build().unwrap();
     union.update(&a).unwrap();
     union.update(&b).unwrap();
     let result = union.to_sketch(true);
@@ -155,7 +167,7 @@ fn estimation_bounds_cover_the_true_union() {
 
 #[test]
 fn union_estimated_size_grows_with_updates() {
-    let mut union = default_union_builder().build();
+    let mut union = default_union_builder().build().unwrap();
     assert_eq!(union.estimated_size(), 2120);
 
     let sketch = tuple_sketch_with_range(0, 1000);


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to