I/O
Running Subprocesses (exec)
How Node.js's child_process.execSync/exec compares to os/exec (Go), std::process::Command (Rust), Process (Swift), and ProcessBuilder (Java).
- Minimum versions
- Node.js ≥ 14.13.1Go ≥ 1.7Rust ≥ 1.71Swift ≥ 5.7Java ≥ 25
- Verified on
- Node.js 24.12.0Go 1.27.1Rust 1.98.1Swift 6.2.4Java 25.0.4.1
Node.js code is an ES module: save it as .mjs or set "type": "module" in package.json.
Running a system command comes in two flavors: wait for it to finish before continuing (sync), or run it without blocking and handle the result later (async). Go doesn't have that distinction: cmd.Run() always blocks the calling goroutine. Rust's std is the same — Command::output() blocks the current thread; real "async" needs tokio::process::Command (not in std). Swift's Process also only has a synchronous API (waitUntilExit()); an "async with timeout" has to be assembled by hand with Task. Java ships both: Process.waitFor() (blocking) and Process.onExit(), which returns a CompletableFuture<Process>.
Running synchronously
import { execSync } from 'node:child_process'
const output = execSync(`echo 'hello world'`)
console.log(output.toString())package main
import (
"fmt"
"os/exec"
)
func main() {
output, err := exec.Command("echo", "hello world").Output()
if err != nil {
panic(err)
}
fmt.Println(string(output))
}use std::process::Command;
fn main() {
let output = Command::new("echo")
.arg("hello world")
.output() // blocks until the child process exits
.unwrap();
print!("{}", String::from_utf8_lossy(&output.stdout));
}import Foundation
let process = Process()
process.executableURL = URL(fileURLWithPath: "/bin/echo")
process.arguments = ["hello world"]
let pipe = Pipe()
process.standardOutput = pipe
try process.run()
process.waitUntilExit() // blocks until the child process exits
let data = pipe.fileHandleForReading.readDataToEndOfFile()
print(String(data: data, encoding: .utf8) ?? "", terminator: "")void main() throws Exception {
Process process = new ProcessBuilder("echo", "hello world").start();
String output = new String(process.getInputStream().readAllBytes());
process.waitFor(); // blocks until the child process exits
IO.print(output);
}hello worldRunning asynchronously with a timeout
import { exec } from 'node:child_process'
import { promisify } from 'node:util'
const execAsync = promisify(exec)
const { stdout, stderr } = await execAsync(`echo 'hello world'`, { timeout: 5000 })
if (stderr) {
console.error(stderr)
}
console.log(stdout)package main
import (
"context"
"os"
"os/exec"
"time"
)
func main() {
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
cmd := exec.CommandContext(ctx, "echo", "hello world")
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
if err := cmd.Run(); err != nil {
panic(err)
}
}// Cargo.toml: tokio = { version = "1", features = ["full"] }
use std::time::Duration;
use tokio::process::Command;
use tokio::time::timeout;
#[tokio::main]
async fn main() {
let child = Command::new("echo")
.arg("hello world")
.stdout(std::process::Stdio::piped()) // without this, stdout is inherited and wait_with_output() returns empty
.kill_on_drop(true) // if the timeout fires, dropping the future kills the child process
.spawn()
.unwrap();
let output = timeout(Duration::from_secs(5), child.wait_with_output())
.await
.expect("timed out")
.unwrap();
print!("{}", String::from_utf8_lossy(&output.stdout));
}import Foundation
let process = Process()
process.executableURL = URL(fileURLWithPath: "/bin/echo")
process.arguments = ["hello world"]
let pipe = Pipe()
process.standardOutput = pipe
try process.run()
let timeoutTask = Task {
try await Task.sleep(for: .seconds(5))
if process.isRunning { process.terminate() } // kill the child process once the timeout fires
}
process.waitUntilExit() // still blocking: Process has no async "wait" API
timeoutTask.cancel()
let data = pipe.fileHandleForReading.readDataToEndOfFile()
print(String(data: data, encoding: .utf8) ?? "", terminator: "")import java.util.concurrent.CompletableFuture;
import java.util.concurrent.TimeUnit;
void main() throws Exception {
Process process = new ProcessBuilder("echo", "hello world")
.redirectErrorStream(true)
.start();
// read stdout concurrently right away — waiting for exit before reading
// can hang the child process once its output buffer fills up
CompletableFuture<String> output = CompletableFuture.supplyAsync(() -> {
try {
return new String(process.getInputStream().readAllBytes());
} catch (Exception e) {
throw new RuntimeException(e);
}
});
try {
process.onExit() // CompletableFuture<Process>, doesn't block the calling thread until .join()
.orTimeout(5, TimeUnit.SECONDS)
.join();
} catch (Exception timedOut) {
process.destroyForcibly(); // kill the child process once the timeout fires
}
IO.print(output.join());
}hello worldReference: github.com/miguelmota/golang-for-nodejs-developers#exec-sync