Runtime APIs
Run child processes
Start another program with Bun.spawn, read its output, and handle its exit code without building a shell command string.
Sometimes your program needs to run another program. A build script calls Git. A maintenance task calls sqlite3. A deploy tool calls ssh. In Bun you do this with Bun.spawn().
Let’s ask Git for its version:
const process = Bun.spawn(['git', '--version'])
const output = await process.stdout.text()
const exitCode = await process.exited
console.log(output.trim())
console.log(`Exit code: ${exitCode}`)
Run it and you get something like:
git version 2.47.1
Exit code: 0
Bun.spawn() starts the process and returns right away. It does not block the JavaScript event loop, so a server can keep answering requests while the child runs. process.stdout is a stream, and text() collects all of it into a string. process.exited is a promise that resolves to the exit code when the child finishes.
Check the exit code
An exit code of 0 means success. Anything else means the command failed, and its output is probably an error message, not the data you wanted. Check the code before trusting the output.
Here we also capture stderr, which is not piped by default:
const process = Bun.spawn(['git', 'status', '--short'], {
stderr: 'pipe',
})
const [exitCode, output, error] = await Promise.all([
process.exited,
process.stdout.text(),
process.stderr.text(),
])
if (exitCode !== 0) {
throw new Error(error.trim())
}
console.log(output)
Run this outside a Git repository and you’ll see it work as intended. Git exits with 128, prints fatal: not a git repository to stderr, and our code throws that message instead of printing an empty result and moving on.
If the executable itself doesn’t exist, Bun.spawn() throws immediately with an ENOENT error. That’s a different failure: not “the command failed” but “there is no such command”. Handle both.
You can set the working directory and environment through options, for example { cwd: '/Users/flavio/bun-notes' }.
Pass an array, not a string
Notice that the command is an array. Each element is one argument. A file name with a space in it stays one argument, because no shell is splitting the text.
This matters for security too. Avoid building a command string from user input and handing it to a shell. A shell interprets characters such as ;, |, and $, so a note title like hello; rm -rf ~ becomes a second command. With an array, that title is just a weird string.
For long-running servers, use the asynchronous Bun.spawn(). There is also Bun.spawnSync(), which blocks the whole process until the child exits. It fits short command-line scripts where waiting is the point, and nowhere else.
Lesson completed