Common mistakes
The traps that actually bite, by what you see: the symptom, why it happens, and
the fix. Error messages are what Deno prints; deno check errors appear in your
editor too.
The program hangs
Nothing reads a command’s output. A child’s stdout is a pipe that holds about 64 KB. A child that writes more waits until someone reads, so this fragment never finishes:
const p = run("seq", "1", "100000");
await p.status; // waits for an exit that never comes
Small output fits in the pipe, which is why the same code “works” in a test and
hangs on real data. Fix: end every command’s pipeline with a consumer,
await run(...).lines.collect(), or .forEach(() => {}) to throw the output
away. .status is for after, or alongside, reading. The same goes for
fnStderr: read stderr to the end, or a child that writes a lot to it blocks.
It finished its work, but doesn’t exit. A command it stopped reading early
is still running, such as a server whose “ready” line .first returned, and
Deno doesn’t exit while a child runs. Wrap the program in main(), which stops
the children on the way out, or stop that one yourself with Deno.kill(p.pid).
A WritableIterable is never closed. The reader waits for more items after
the last one. Call close() when the data ends, and close(error) when it
fails.
It throws
ExitCodeError: grep exited with code 1. grep exits 1 when nothing matches,
and proc treats every non-zero exit as a failure. So do diff (files differ),
cmp, and test. Fix: an fnError handler that lets code 1 through, as in
Searching logs, or catch
ExitCodeError and check .code.
RangeError: .first: the sequence is empty. .first on a sequence with no
items: a command that printed nothing and succeeded (one that failed throws its
ExitCodeError instead), or a filter that matched nothing. .first never
resolves to undefined. When the output may be empty, take an array of at most
one:
import { run } from "@j50n/proc";
// find succeeds and prints nothing when no file matches.
const find = ["find", ".", "-name", "*.bak"] as const;
try {
console.log(await run(...find).lines.first);
} catch (error) {
console.log(String(error));
}
const [path] = await run(...find).lines.take(1).collect();
console.log(path ?? "no backups");
RangeError: .first: the sequence is empty
no backups
NotFound: Failed to spawn 'ls -la': entity not found. The whole command
was passed as one string, so Deno looked for a program named ls -la. Pass each
argument separately: run("ls", "-la"). The same error, with a real name, means
the program isn’t installed or isn’t on PATH.
NotCapable: Requires run access to "grep", run again with the --allow-run flag.
Deno’s permissions. Grant what the script uses: --allow-run=grep,
--allow-read=./logs, --allow-write=out.txt.
TypeError: cache needs Deno KV from cache(). cache uses Deno KV, which
is unstable. Run with --unstable-kv, or add "unstable": ["kv"] to
deno.json.
BrokenPipe: Broken pipe (os error 32) when you pipe the script’s output
into head or less and quit early, from a write of your own to Deno.stdout.
toStdout() and writeTo(Deno.stdout.writable) stop quietly instead, and
console.log ignores it. Use one of those, or catch it:
if (!(error instanceof Deno.errors.BrokenPipe)) throw error;.
SyntaxError: Unexpected end of JSON input from jsonParse. A blank line
is not JSON. Filter blank lines out before it, or use fromJsonToRows() from
@j50n/proc/transforms, which skips them.
The error says exited with code 1 and nothing else. The command’s own
explanation went to stderr, which is your terminal by default. To put it in the
error, capture it with fnStderr and rethrow from fnError; see
Errors.
It doesn’t type-check
This expression is not callable because it is a 'get' accessor. Did you mean to use it without '()'?
(TS6234). .lines, .chunkedLines, .first, .status, and .pid are
properties: await run("ls").lines.first, not .lines().first(). Without type
checking, the same mistake fails at run time with
TypeError: run(...).lines is not a function.
This comparison appears to be unintentional because the types 'string[]' and 'string' have no overlap.
(TS2367), on a row of parsed data. The parsers yield batches of rows, so without
.flatten() each item is an array of rows, not a row. Where the types don’t
catch it, the counts are wrong:
import { read } from "@j50n/proc";
import { fromCsvToRows } from "@j50n/proc/transforms";
const batches = await read("people.csv").transform(fromCsvToRows()).count();
const rows = await read("people.csv").transform(fromCsvToRows()).flatten()
.count();
console.log({ batches, rows });
{ batches: 1, rows: 3 }
Add .flatten() after the parser. toJson() takes one value per item, so
flatten before it too: a batch left whole is written as one JSON array.
'error' is of type 'unknown'. (TS18046), on error.code in a catch.
Narrow first:
if (error instanceof ExitCodeError) console.log(error.code); else throw error;.
Module '".../mod.ts"' has no exported member 'fromCsvToRows'. The data
transforms are a separate entry point: import them from
"@j50n/proc/transforms".
Property 'collect' does not exist on type 'never'. .lines and
.chunkedLines need bytes, and .run() needs strings or bytes; on other items
the type is never. Strings are lines already, so drop the .lines; before a
.run(), turn numbers or objects into strings with .map(String) or
.transform(jsonStringify).
Wrong results, no error
The file came out empty. writeTo(path) empties the file before anything is
read, so a pipeline that reads the same file finds nothing. Pass
{ atomic: true }, which writes a new file and renames it over the old one; see
Files.
The second pass finds nothing. An Enumerable is used once; a second
collect() on it, or on another chain built from it, yields nothing and throws
nothing. Collect into an array first, or split with tee(). See
Key ideas.
A failed command didn’t throw. Stopping early (.first, take, find,
break) closes the pipeline without checking the exit code:
import { run } from "@j50n/proc";
const cmd = ["sh", "-c", "echo partial; exit 3"] as const;
console.log(await run(...cmd).lines.first); // no error: the exit code is unread
try {
await run(...cmd).lines.collect(); // reads to the end, so it checks
} catch (error) {
console.log(String(error));
}
partial
ExitCodeError: sh exited with code 3
Read the output to the end when the exit code matters. A command started with
run() and never consumed isn’t checked either: the script waits for it to
exit, then ignores how.
A result is a Promise { <pending> }. Every consumer (collect, forEach,
count, first, …) returns a promise; await it. An un-awaited forEach
may not have run when the next line does.
Memory keeps growing with a WritableIterable. write() returns at once,
without waiting for the reader; there is no backpressure:
import { WritableIterable } from "@j50n/proc";
const queue = new WritableIterable<number>();
for (let n = 1; n <= 3; n++) {
await queue.write(n); // resolves at once; nothing has read it
console.log(`wrote ${n}`);
}
await queue.close();
for await (const n of queue) console.log(`read ${n}`);
wrote 1
wrote 2
wrote 3
read 1
read 2
read 3
A producer that outruns its reader fills memory. When the source can wait (a
file, a socket, a paginated API), don’t push into a queue; pull from it with an
async generator, or enumerate() a ReadableStream, so it is read only as fast
as you consume it.
It’s slow
A pipeline of many short lines crawls, especially one that feeds a command
with .run(): every step, and every write to the command, happens once per
line. Use .chunkedLines instead of .lines, and work on each array:
.chunkedLines.map((lines) => lines.filter(...)).run("sort"). On two million
lines that took a filter between two commands from 16 seconds to 0.3. See
Lots of lines.
Children die without cleaning up
In a container, stopping it sends SIGTERM to Deno, Deno exits at once, and the
container’s end kills the children before their cleanup runs: temporary files
are left behind, uploads cut off, locks held. On any host, an uncaught error
does the same: Deno kills its children as it exits. Wrap the program in
main(), which passes the signal on and waits for the children (30 seconds by
default) before exiting. See Shutting down cleanly.