Course outline

Dart / Integration: targets, resources, validation, and evidence / Lesson 14

Own resources and respect byte and text boundaries

Design acquisition, completion, cleanup, text decoding, and bounded consumption while keeping dart:io assumptions platform-specific.

About 5 min readContent reviewed
On this page

Lesson goals

  • Distinguish write acceptance, asynchronous completion, and resource closure.
  • Decode streaming text across byte chunks without assuming each chunk is a complete character or line.

Name the resource owner before introducing I/O

Files, sinks, sockets, and subscriptions have lifetimes beyond a single method call. An API should say whether it borrows a resource from the caller or owns and closes it. dart:io is intended for supported non-browser platforms; importing it is not a portable browser-file abstraction. Paths, permissions, encodings, and failure behavior depend on the real environment. This lesson performs no file, directory, network, or host operations. It uses a local recording model and fixed byte chunks to explain ownership and decoding without pretending those models verify an actual operating-system resource.

Keep cleanup tied to the scope that owns the object

The recording sink below rejects writes after closure and records whether its owner closed it. finally establishes a local cleanup path even if the body later becomes capable of throwing. This model has synchronous close and no real I/O, so it cannot prove that a native IOSink flushed data or persisted it to disk. A real asynchronous cleanup method may return a Future that the owner must await. Keep the primary failure and any cleanup failure distinguishable rather than replacing every error with a reassuring closed marker.

dart A local ownership and closure model
class RecordingSink {
  final lines = <String>[];
  bool closed = false;
  void write(String text) {
    if (closed) throw StateError('sink closed');
    lines.add(text);
  }
  void close() { closed = true; }
}
void main() {
  final sink = RecordingSink();
  try {
    sink.write('batch:B-4');
  } finally {
    sink.close();
  }
  print(sink.lines.single);
  print(sink.closed);
}
Expected output
batch:B-4
true

Expected output is batch:B-4 then true. The list records one string and the flag records model closure only. No file, native sink, or runtime test is involved.

A write call is not a durability certificate

An IOSink can accept bytes or encode written strings, while its asynchronous completion and error channels need the documented handling. flush and close have particular API meanings; neither should be advertised as a universal transactional durability guarantee. Observe errors promptly instead of assuming a synchronous write call reports every later failure. If an operation writes a complete report, define whether partial output can remain and how a caller distinguishes completion from acceptance. Resource release and business success are related but separate outcomes, and finally running does not automatically make a partially completed operation safe to retry.

Text boundaries do not follow byte chunk boundaries

A UTF-8 character can span multiple incoming chunks, and a line can span many decoded chunks. Decode a byte stream with the streaming UTF-8 decoder, then split decoded text into lines. Decoding each chunk independently can reject or corrupt a valid character divided across the boundary. The example deliberately splits the two-byte encoding of é and supplies a newline in the second chunk. It uses only in-memory data and library transformations. A real input also needs a policy for malformed encoding, line size, total size, and whether sensitive content may enter diagnostics.

dart Decode a character split across chunks
import 'dart:async';
import 'dart:convert';
Future<void> main() async {
  final bytes = Stream<List<int>>.fromIterable(<List<int>>[
    <int>[0xC3],
    <int>[0xA9, 0x0A],
  ]);
  final lines = bytes.transform(utf8.decoder).transform(const LineSplitter());
  await for (final line in lines) {
    print(line);
  }
}
Expected output
é

Expected output is é on one line. The decoder retains the partial UTF-8 sequence between chunks. No external bytes were read and the Dart source was not executed.

Streaming still needs size and failure limits

Using a stream does not by itself bound memory. toList can materialize an entire source, a huge line can accumulate before splitting, and paused delivery can buffer upstream data. Set limits that match the product’s expected input and reject or stop deliberately when they are exceeded. For a real resource, verify successful completion, mid-stream error, early consumer exit, and cleanup failure in an authorized isolated environment. Record target and encoding assumptions. This course makes no file integrity, permission, persistence, or resource-release execution claim; the shown outputs remain analytical expectations.

Common errors and fixes

A valid UTF-8 character fails when chunks are decoded separately.

Why it happens
A multibyte sequence was split across chunk boundaries.
How to fix it
Use the streaming decoder before applying line splitting.
Prevent it next time
Include a split multibyte character in isolated decoding fixtures.

The owner returns before asynchronous closure completes.

Why it happens
Cleanup initiation was confused with completion.
How to fix it
Await the real resource’s required cleanup Future and handle its failure.
Prevent it next time
Document borrowed versus owned resources and their close contracts.

A stream-based parser exhausts memory on one huge input.

Why it happens
Streaming syntax was mistaken for bounded buffering and result size.
How to fix it
Bound accumulated lines, total input, and materialized results.
Prevent it next time
Test oversized and early-stop paths, not only small successful input.

Check your understanding

1. Why should an API accepting a caller-owned sink avoid closing it silently?

Show worked solution

The caller may intend to write additional data or manage the sink across several operations. Closing it changes ownership and can break later use. State whether the API borrows or owns the sink, and let the declared owner perform and await closure. If ownership transfer is intended, make that part of the contract rather than inferring it from a convenient finally block.

2. Does a successful flush prove a complete report can survive every process or machine failure?

Show worked solution

No. Interpret the specific API guarantee and underlying storage behavior rather than projecting a universal durability promise onto it. A complete report can require an application-level write protocol, integrity checks, and recovery policy. This lesson demonstrates local ownership and streaming decode semantics only, not a filesystem transaction or power-loss test.

Key takeaways

  • Resource ownership, completion, durability, and cleanup are separate claims.
  • Decode text incrementally and bound every accumulated representation.

Official references