# How iloader Handles Errors During Device Discovery to Prevent List Poisoning

> Discover how iloader prevents list poisoning by isolating device discovery errors. Its Rust backend returns individual Results, enabling the React frontend to filter valid devices and report errors separately.

- Repository: [Nicholas Sharp/iloader](https://github.com/nab138/iloader)
- Tags: internals
- Published: 2026-09-13

---

**iloader isolates failures during iOS device enumeration by returning a vector of individual `Result` objects from its Rust backend, allowing the React frontend to filter out broken devices and display only valid entries while reporting errors separately via toast notifications.**

When building desktop applications that interact with physical hardware, a single unresponsive device can traditionally crash or corrupt the entire discovery workflow. The **nab138/iloader** repository solves this by implementing a defensive error-handling strategy that ensures how iloader handles errors during device discovery keeps the user interface clean and reliable even when multiple devices fail to respond.

## Backend Strategy: Returning Individual Results Instead of Failing Fast

The core protection against list poisoning resides in [`src-tauri/src/device.rs`](https://github.com/nab138/iloader/blob/main/src-tauri/src/device.rs), specifically within the `list_devices` Tauri command. Rather than aborting enumeration when encountering a faulty iOS device, the backend creates a **vector of `Result<DeviceInfo, AppError>`**—one entry per discovered device.

### The list_devices Command Implementation

The function signature explicitly declares its failure-isolating contract: it returns a `Vec` where each element is either a successful `DeviceInfo` or an `AppError` specific to that device.

```rust
#[tauri::command]
pub async fn list_devices() -> Result<Vec<Result<DeviceInfo, AppError>>, AppError> {
    // ...
    let device_info_futures = devs.iter().map(|d| async move {
        // ...
        let mut lockdown_client = LockdownClient::connect(&provider).await
            .map_err(|e| AppError::DeviceComsWithMessage(
                "Unable to connect to lockdown".into(),
                e.to_string(),
            ))?;
        // ...
        Ok(DeviceInfo { /* … */ })
    });
    // ...
    let device_infos = futures::future::join_all(device_info_futures).await;
    Ok(device_infos)
}

```

Each device connection attempt runs as an independent asynchronous future. By using `futures::future::join_all`, the command aggregates results without short-circuiting; a failure in one future (such as an inability to establish a lockdown client connection) converts to an `Err` variant in the vector rather than propagating up to abort the entire operation. This pattern ensures that low-level communication errors—defined in [`src-tauri/src/error.rs`](https://github.com/nab138/iloader/blob/main/src-tauri/src/error.rs) as `AppError` variants—are captured per-device and serialized for the frontend.

## Frontend Filtering: Selecting Only Valid Devices

The frontend component at [`src/Device.tsx`](https://github.com/nab138/iloader/blob/main/src/Device.tsx) consumes the nested results and implements the final guard against list pollution. It iterates over the returned array and **only appends successful `Ok` entries** to the React state, explicitly ignoring `Err` variants for the device list itself.

### Processing the Result Array

The TypeScript code handles the discriminated union type to separate valid devices from failures:

```tsx
const results = await invoke<Array<{ Ok: DeviceInfo } | { Err: AppError }>>("list_devices");
for (const result of results) {
  if ("Ok" in result) {
    devices.push(result.Ok);               // ← only good devices are kept
  } else if ("Err" in result) {
    toast.error(err(t("device.unable_load_devices_prefix"), result.Err));
  }
}

```

This filtering logic guarantees that malformed or unreachable devices never appear in the UI list. The implementation uses a functional approach to extract valid entries:

```tsx
// Front‑end: request device list and ignore failing entries
async function refreshDevices() {
  const results = await invoke<Array<{ Ok: DeviceInfo } | { Err: AppError }>>("list_devices");
  const good = results.filter((r) => "Ok" in r).map((r) => (r as { Ok: DeviceInfo }).Ok);
  setDevices(good);
}

// Back‑end: return a per‑device Result so the caller can filter
#[tauri::command]
pub async fn list_devices() -> Result<Vec<Result<DeviceInfo, AppError>>, AppError> { 
    // Implementation returns join_all results
}

```

## Error Isolation and User Feedback

While broken devices are excluded from the list, users are not left without information. The frontend logs each `Err` variant using a toast notification system, providing visibility into which specific devices failed and why. The `AppError` type—defined in [`src/errors.tsx`](https://github.com/nab138/iloader/blob/main/src/errors.tsx) for the TypeScript side and [`src-tauri/src/error.rs`](https://github.com/nab138/iloader/blob/main/src-tauri/src/error.rs) for serialization—carries structured error messages from the Rust layer to the UI. This dual-channel approach ensures that **diagnostics reach the user without corrupting the data model** displayed in the device selector.

## Summary

- **iloader** wraps each device enumeration attempt in an individual `Result` type to isolate hardware communication failures.
- The Rust backend returns `Vec<Result<DeviceInfo, AppError>>`, enabling partial success where working devices are reported alongside failures rather than being blocked by them.
- The frontend filters the result array for `Ok` variants only, ensuring broken or unresponsive iOS devices never appear in the discovery list.
- Errors are surfaced through toast notifications using the `AppError` type, providing debugging context while maintaining UI integrity.

## Frequently Asked Questions

### What happens when iloader encounters a broken iOS device during discovery?

The device-specific future returns an `Err(AppError)` which is collected into the results vector. The frontend detects this variant, displays a toast notification with the error details, and excludes the entry from the device list, preventing any "ghost" or broken entries from appearing in the UI.

### How does the Rust backend prevent one device failure from aborting the entire list?

The `list_devices` function uses `futures::future::join_all` to wait for all per-device connection futures simultaneously. Because each future handles its own error mapping (converting connection failures to `AppError` variants), a single failure does not short-circuit the `await`. The function returns a mixed vector containing both successful `DeviceInfo` objects and error variants.

### What data structure does iloader use to transport per-device errors to the frontend?

iloader uses a JSON serialization of Rust's `Result<T, E>` type, represented in TypeScript as a discriminated union `Array<{ Ok: DeviceInfo } | { Err: AppError }>`. This allows the frontend to use the `"Ok" in result` check to distinguish between valid devices and errors received from the Tauri command.

### Where is the device discovery error handling implemented in the codebase?

The error handling logic is split between [`src-tauri/src/device.rs`](https://github.com/nab138/iloader/blob/main/src-tauri/src/device.rs) (lines 36-40 and 68-74), which implements the `list_devices` command and per-device error wrapping, and [`src/Device.tsx`](https://github.com/nab138/iloader/blob/main/src/Device.tsx) (lines 99-108), which filters the results and manages UI state. The error type definitions live in [`src-tauri/src/error.rs`](https://github.com/nab138/iloader/blob/main/src-tauri/src/error.rs) and [`src/errors.tsx`](https://github.com/nab138/iloader/blob/main/src/errors.tsx).