Typorithm · medium

11. unknown errors and explicit result types

typescriptstable-typescriptinterviewindependent-practice
Saved on this device
Full lesson · 4 learning steps Open any exercise for focused practice
1

Step 1 of 4 · intro card

11. unknown errors and explicit result types

0%

Concept and contract

Separate expected failures from thrown values and preserve exhaustive error handling.

JavaScript can throw any value, including text, null or an object unrelated to Error. A strict catch variable is therefore unknown until checked. Reading message without narrowing makes an unjustified assumption about the source of the exception.

Expected domain failures can be represented with a discriminated Result instead of throwing. Reserve exceptions for failures that the current layer cannot sensibly recover from, or follow the project’s established convention. The important point is an explicit contract, not a universal ban on exceptions.

Mechanism and worked example

The example parses a positive integer into an ok or error branch.

The example parses a positive integer into an ok or error branch. Number conversion alone would accept empty text as zero and fractions as numbers; the validator specifies the business rule with integer and positivity checks. Consumers must inspect ok before reading value.

The catch helper handles Error specially and provides a safe fallback for other thrown values. It does not serialize arbitrary objects, which could expose sensitive details or itself fail. User-facing messages and diagnostic details deserve separate policies in production.

Complete worked example

typescript

type Result = { ok: true; value: number } | { ok: false; reason: string };
function positive(text: string): Result {
  const value = Number(text);
  return Number.isInteger(value) && value > 0 ? { ok: true, value } : { ok: false, reason: "Positive integer required" };
}
function errorMessage(error: unknown): string { return error instanceof Error ? error.message : "Unexpected failure"; }
Reasoning
Conversion and validation are separate operations; the tagged union preserves which result exists.

Counterexamples and common mistakes

The rejected access tries to read message directly from unknown.

The rejected access tries to read message directly from unknown. Casting every caught value to Error does not make it an Error instance. Likewise, a Result with optional value and optional error would permit contradictory states and weaken exhaustive handling.

Do not swallow all exceptions and return success with an empty value. That changes failure into misleading data. Preserve an actionable error category or message, and avoid adding a broad catch around code whose failures should reach the caller or error boundary.

Rejected counterexample: expected compiler diagnostic

typescript

function unsafe(error: unknown): string {
  // @ts-expect-error An arbitrary thrown value need not have message.
  return error.message;
}
Reasoning
Thrown values are unknown until runtime evidence establishes a property contract.

Independent practice

Implement parseLimit for unknown input.

Implement parseLimit for unknown input. Accept only an integer number between one and one hundred; reject numeric strings, NaN and out-of-range values. Return a tagged result whose successful branch requires value and whose failure branch requires a reason.

Test both boundaries, a fraction and a string. Write a caller that reads the value only in the successful branch. Explain why accepting a numeric string would be a deliberate coercion policy rather than a consequence of the number annotation.

Step 1: Check the input is numeric and integral. Step 2: Return the appropriate tagged result for the bounds.

Practice starter: solve before reading the solution

typescript

type Limit = { ok: true; value: number } | { ok: false; reason: string };
function parseLimit(input: unknown): Limit { throw new Error("TODO"); }
Reasoning
Reject coercion unless the public parser contract explicitly allows it.
  • Hint 1: One and one hundred are accepted boundaries.
  • Hint 2: Numeric strings are rejected under this contract.

Solution, tests and trade-offs

The solution combines primitive checking with a bounded integer predicate.

The solution combines primitive checking with a bounded integer predicate. A tagged return makes the successful value available without a non-null assertion. The error branch remains explicit and can later be extended with stable error codes rather than parsing English messages.

Runtime assertions cover malformed input and boundaries. Add a type test that accessing value before narrowing is rejected. If new error categories are introduced, test the consumer’s handling rather than merely checking that the parser still returns an object.

Practice solution and acceptance checks

typescript

type Limit = { ok: true; value: number } | { ok: false; reason: string };
function parseLimit(input: unknown): Limit {
  return typeof input === "number" && Number.isInteger(input) && input >= 1 && input <= 100
    ? { ok: true, value: input } : { ok: false, reason: "Integer from 1 to 100 required" };
}

// Acceptance checks
for(const x of [1,100])if(!parseLimit(x).ok)throw new Error("valid");for(const x of [0,101,1.5,"2",NaN])if(parseLimit(x).ok)throw new Error("invalid");
Reasoning
The bounds and integer check are runtime rules; the result type preserves their outcome.

Interview reasoning and next step

In interviews, discuss when Result improves composition and when an exception fits an existing call chain.

In interviews, discuss when Result improves composition and when an exception fits an existing call chain. Explain that TypeScript does not encode checked exceptions in ordinary function signatures, so a return annotation does not prove the implementation cannot throw.

Continue with module boundaries. Public result types should be exported deliberately and imported as types where appropriate, so error contracts remain usable without creating accidental runtime dependency cycles.

Optional stretch — after the core task works: Replace the reason text with stable error codes.

Separate expected failures from thrown values and preserve exhaustive error handling.