facebook/flow · error

LwtSysUtils.exec spawn failed; OCaml propagates as rejected

Error message

LwtSysUtils.exec spawn failed; OCaml propagates as rejected Lwt promise

What it means

Panics when the async Command's output() future completes with an Err — the process could not be spawned at all. The message notes the OCaml Lwt original rejects a promise that callers catch; the Rust port panics instead. Spawn errors are almost always NotFound (binary missing from PATH) or PermissionDenied (not executable), not failures of the child itself.

Source

Thrown at rust_port/crates/flow_lwt_sys_utils/src/lib.rs:47

    env: Option<&[(String, String)]>,
    cwd: Option<&str>,
    cmd: &str,
    args: &[&str],
) -> CommandResult {
    let mut command = tokio::process::Command::new(cmd);
    command.args(args);
    if let Some(envs) = env {
        for (k, v) in envs {
            command.env(k, v);
        }
    }
    if let Some(cwd) = cwd {
        command.current_dir(cwd);
    }
    let output = command
        .output()
        .await
        .expect("LwtSysUtils.exec spawn failed; OCaml propagates as rejected Lwt promise");
    CommandResult {
        stdout: String::from_utf8_lossy(&output.stdout).to_string(),
        stderr: String::from_utf8_lossy(&output.stderr).to_string(),
        status: output.status,
    }
}

View on GitHub (pinned to f88ac94bcf)

Solutions

  1. Install the missing tool or add its directory to PATH for the process
  2. Pre-resolve the binary to an absolute path before spawning
  3. Preflight-check availability (which-style lookup) and degrade gracefully when the tool is optional

Example fix

// before
let output = command.output().await
    .expect("LwtSysUtils.exec spawn failed; OCaml propagates as rejected Lwt promise");

// after — mirror the OCaml rejected-promise contract
let output = match command.output().await {
    Ok(o) => o,
    Err(e) => return Err(FlowLspError::ExecFailed { program: program.into(), source: e }),
};
Defensive patterns

Strategy: validation

Validate before calling

// Preflight: is the binary resolvable on PATH before spawning?
fn binary_available(bin: &str) -> bool {
    std::env::var_os("PATH")
        .map(|paths| {
            std::env::split_paths(&paths).any(|dir| {
                let p = dir.join(bin);
                p.is_file() && p.metadata().map(|m| !m.permissions().readonly()).unwrap_or(false)
            })
        })
        .unwrap_or(false)
}

Try / catch

match command.output().await {
    Ok(output) => output,
    Err(e) if e.kind() == std::io::ErrorKind::NotFound => {
        return Err(FlowLspError::ToolMissing { program: program.into() });
    }
    Err(e) => return Err(FlowLspError::ExecFailed { program: program.into(), source: e }),
}

Prevention

When it happens

Trigger: Calling exec for an external tool (git, watchman, shell) that is not installed or not on PATH under the current environment; running under cron/systemd/container where PATH is minimal; the target file exists but lacks the execute bit.

Common situations: Minimal Docker images without git; systemd services with PATH=/usr/bin only; watchman absent on CI; scripts passing a relative binary name resolved differently in prod.

Related errors


AI-assisted analysis of facebook/flow@f88ac94bcf (2026-08-20). Data as JSON: /api/errors/031a47708b90738a. Report an issue: GitHub.