Make the app reliable

Treat child output as a protocol

Define stable machine-readable output instead of scraping prose meant for a person.

12 minute lesson

~~~

The previous module launched tools and read their output in one gulp. That breaks down the moment you need progress — a long export where the interface shows a moving bar. Now the child’s stdout is not text you glance at. It is an interface between two programs, and it deserves the same care as a network API.

Scraping human-oriented output is the tempting shortcut. The tool prints Processed 12 of 40 files, so you match it with a regex and ship. Then a later version of the tool rewords its messages, your regex silently stops matching, and the progress bar freezes at zero while the work completes fine. Nobody wrote a bug. A sentence changed.

The fix is to define the messages. If you control the tool, make it emit JSON Lines — one JSON object per line — on stdout, and keep human diagnostics on stderr:

{"event":"progress","completed":12,"total":40}
{"event":"finished","output":"recording.mp4"}

Each line stands alone. Decode complete lines into a typed value so unknown input fails loudly at the boundary:

struct ToolEvent: Decodable {
  let event: String
  let completed: Int?
  let total: Int?
  let output: String?
}

func parse(line: Data) throws -> ToolEvent {
  try JSONDecoder().decode(ToolEvent.self, from: line)
}

Streaming reads deliver arbitrary chunks, not lines. A read can end mid-message, so buffer until a newline appears:

var buffer = Data()

func receive(_ chunk: Data) {
  buffer.append(chunk)
  while let newline = buffer.firstIndex(of: UInt8(ascii: "\n")) {
    let line = buffer[..<newline]
    buffer.removeSubrange(...newline)
    if let event = try? parse(line: line) {
      handle(event)
    }
  }
}

Decoding failures at this boundary are information. Keep a bounded tail of recent raw lines — the last hundred, say — so when the child fails you can show or log what it actually said, instead of a bare “exit code 1”.

If the protocol will evolve, add a version field to the first message and reject major versions you do not understand. That turns a future incompatibility into a clear error instead of a subtle misparse.

Verify with hostile input. Feed the parser a message split across two chunks, two messages in one chunk, and a line of plain prose. The first two must decode correctly. The third must be rejected without taking the app down. If your parser only handles one-message-per-read, it works in tests and fails under real pipe timing.

Lesson completed

Take this course offline

Get every free book and course as PDF and EPUB files.

Get the download library →