Async Ports & Dependencies

The Course
•
5 min read

A port is a small interface describing a dependency your application needs. type Transport = () => Promise<unknown> says the transport asynchronously supplies untrusted data. A fixture, HTTP adapter or test double can implement that function without changing the caller.

Do not annotate a transport as Promise<Book[]> just because you hope a server returns books. A real transport can successfully deliver malformed data. Decode after awaiting it, then return a trusted result to the application. A transport rejection and a successful invalid response are different failures.

Keep try/catch narrow: catch only the transport call, then run your decoder outside that catch. Otherwise a programming defect in the decoder might be relabeled as an ordinary offline error. Catch values are unknown; use instanceof Error before reading message, or choose a stable generic message.

This course’s capstone injects a local transport and does not make HTTP requests. A real fetch adapter also needs status handling and JSON decoding, taught in JavaScript intermediate. Typing an adapter does not implement that behavior for you.

Predict, run, and explain

Write the output and trace the important state changes before checking it. Then run the program. Use Check types first; execution is allowed only when the real compiler accepts the source.

Predict, then runTypeScript
type Transport = () => Promise<unknown>;

async function readText(transport: Transport): Promise<string> {
  let value: unknown;

  try {
    value = await transport();
  } catch {
    return "Transport failed";
  }

  return typeof value === "string" ? value.trim() : "Invalid text";
}

console.log(await readText(async () => " Types "));
console.log(await readText(async () => 25));
console.log(
  await readText(async () => {
    throw "offline";
  })
);

Enable JavaScript to edit and type-check this example.

Check your prediction
Types
Invalid text
Transport failed

A transport port should retain uncertainty

An injected transport separates how data arrives from what valid data means. Promise<unknown> honestly represents an untrusted payload, even when a fixture happens to return a perfectly shaped record. Decode after awaiting it instead of asserting the server contract true.

Keep the catch around the transport operation only when that is the failure you intend to convert. Otherwise a decoder programming defect can be mislabeled as an ordinary offline error, hiding the actual problem from both tests and users.

A transport port should retain uncertainty: traceTypeScript
type Transport = () => Promise<unknown>;

async function read(transport: Transport): Promise<string> {
  let value: unknown;

  try {
    value = await transport();
  } catch {
    return "Transport failed";
  }

  return typeof value === "string" ? value.trim() : "Invalid data";
}

console.log(await read(async () => " Types "));
console.log(await read(async () => 25));

Enable JavaScript to edit and type-check this example.

Check your prediction
Types
Invalid data

Both transports fulfill; only one returns valid data for this reader. Network success and domain success are independent facts.

A transport port should retain uncertainty: contrastTypeScript
async function read(transport: () => Promise<unknown>): Promise<string> {
  let value: unknown;

  try {
    value = await transport();
  } catch {
    return "Transport failed";
  }

  function decode(input: unknown): string {
    throw new Error("Decoder defect");
  }

  return decode(value);
}

try {
  await read(async () => "Types");
} catch (error) {
  console.log(error instanceof Error ? error.message : "Unknown");
}

Enable JavaScript to edit and type-check this example.

Check your prediction
Decoder defect

The deliberate decoder defect escapes the narrow transport catch. A test can therefore distinguish a collaborator outage from a bug in domain code.

Try the rule on new inputs

Use transport rejection, successful invalid data and a throwing decoder as three separate fixtures. Give each a distinct observed outcome and verify no broad catch erases the distinction.

Your turn

Exercise

Async Ports & Dependencies: practise

Write readCount(transport) returning Promise<Result<number>>. Transport failure returns message Unavailable; a non-number or invalid nonnegative integer returns Invalid count. Print 0, Invalid count and Unavailable for three fixtures.

Your workspaceTypeScript
// Give fixtures the same Transport contract and decode only after success.

Enable JavaScript to edit and type-check this example.

Hint 1: the idea

The transport’s type preserves uncertainty rather than pretending data was validated.

Hint 2: a first step

Use Promise<unknown> for the dependency, then a Result<number> after numeric checks.

Show one solution
One possible solutionTypeScript
type Result<T> =
  | {
      ok: true;

      value: T;
    }
  | {
      ok: false;

      message: string;
    };

type Transport = () => Promise<unknown>;

async function readCount(transport: Transport): Promise<Result<number>> {
  let value: unknown;

  try {
    value = await transport();
  } catch {
    return { ok: false, message: "Unavailable" };
  }

  if (
    typeof value !== "number" ||
    !Number.isFinite(value) ||
    !Number.isInteger(value) ||
    value < 0
  ) {
    return { ok: false, message: "Invalid count" };
  }

  return { ok: true, value };
}

for (const transport of [
  async () => 0,
  async () => "0",
  async () => {
    throw "offline";
  }
]) {
  const result = await readCount(transport);

  console.log(result.ok ? result.value : result.message);
}

Enable JavaScript to edit and type-check this example.

0
Invalid count
Unavailable

Explain why the implementation meets the stated contract. Which part is enforced by the language or checker, and which part needs behavioral evidence?

Change it and test it

Test null, negative and fractional payloads. Then make a decoder deliberately throw a programming error and explain why a broad catch around the whole pipeline would hide the source of the problem. Distinguish trusted fixtures from validated production inputs.

Was this article helpful?

Your response is saved on this device.