nautechsystems/nautilus_trader · error · anyhow::Error

prepare_custom_data_batch called with empty data

Error message

prepare_custom_data_batch called with empty data

What it means

prepare_custom_data_batch requires at least one CustomData item to derive the type name, identifier, metadata, and timestamp bounds from the first element. An empty Vec cannot produce a record batch, so it is rejected upfront rather than failing later during encoding.

Source

Thrown at crates/persistence/src/backend/custom.rs:136

            for segment in safe.split('/') {
                components.push(segment.to_string());
            }
        }
    }
    components
}

/// Prepares a batch of custom data for writing: encodes to Arrow, augments with `data_type` column,
/// and returns type identity and timestamp range so the catalog can build path and perform I/O.
///
/// # Errors
///
/// Returns an error if encoding or augmentation fails, or if the type is not registered.
pub fn prepare_custom_data_batch(
    data: Vec<CustomData>,
) -> anyhow::Result<(RecordBatch, String, Option<String>, UnixNanos, UnixNanos)> {
    let Some(first_custom) = data.first() else {
        anyhow::bail!("prepare_custom_data_batch called with empty data");
    };

    let type_name = first_custom.data.type_name();
    let identifier = first_custom.data_type.identifier().map(String::from);
    let dt_meta = first_custom.data_type.metadata_string_map();
    let data_type_json = first_custom
        .data_type
        .to_persistence_json()
        .map_err(|e| anyhow::anyhow!("Failed to serialize data_type for persistence: {e}"))?;

    let start_ts = first_custom.data.ts_init();
    let end_ts = data.last().map_or(start_ts, |custom| custom.data.ts_init());
    let items: Vec<Arc<dyn CustomDataTrait>> =
        data.into_iter().map(|c| Arc::clone(&c.data)).collect();

    let batch = encode_custom_to_arrow(type_name, &items)
        .map_err(|e| anyhow::anyhow!("Failed to encode custom data to Arrow: {e}"))?
        .ok_or_else(|| {

View on GitHub (pinned to 18893faf8b)

Solutions

  1. Check data.is_empty() before calling write_custom_data_batch and return early when empty.
  2. Guard the call site so only non-empty batches are written.
  3. If empty writes should be valid, treat them as no-ops at the caller instead of invoking the API.

Example fix

// before
write_custom_data_batch(data)?;
// after
if !data.is_empty() {
    write_custom_data_batch(data)?;
}
Defensive patterns

Strategy: validation

Validate before calling

if data.is_empty() {
    return; // nothing to write — skip the API call entirely
}
write_custom_data_batch(data)?;

Try / catch

if let Err(e) = write_custom_data_batch(data) {
    if e.to_string().contains("empty data") {
        // treat as no-op; log and continue
    } else {
        return Err(e);
    }
}

Prevention

When it happens

Trigger: Calling write_custom_data_batch with an empty Vec<CustomData>; the writer passes the batch through to prepare_custom_data_batch which bails on data.first() being None.

Common situations: A query/backfill produced no rows but the code unconditionally calls the write API, a filtered stream yielded nothing for a session, or an off-by-one slice produced an empty collection.

Understand the failure class

Background: "must not be empty", "cannot be empty" — required-field validation errors across open-source libraries — this error's family across 41 libraries.

Related errors


AI-assisted analysis of nautechsystems/nautilus_trader@18893faf8b (2026-09-08). Data as JSON: /api/errors/91f2acbad0f18473. Report an issue: GitHub.