How iloader Handles Errors During Device Discovery to Prevent List Poisoning

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, 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.

#[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 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 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:

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:

// 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 for the TypeScript side and 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 (lines 36-40 and 68-74), which implements the list_devices command and per-device error wrapping, and 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 and src/errors.tsx.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →