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
- Install the missing tool or add its directory to PATH for the process
- Pre-resolve the binary to an absolute path before spawning
- 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
- Pre-resolve external tools to absolute paths at startup
- Set an explicit PATH in service/container environments that run flow
- Degrade gracefully when an optional tool (e.g. watchman) is absent
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
- Daemon::spawn: timed out waiting for child to connect
- lock file is already held
- invalid line
- invalid column
- failed to write cli errors
AI-assisted analysis of facebook/flow@f88ac94bcf (2026-08-20).
Data as JSON: /api/errors/031a47708b90738a.
Report an issue: GitHub.