Skip to content

Error Handling

Targo follows Go-style error handling. Ordinary failures are returned as values, not thrown as exceptions.

Core Rules

  • Prefer [T, error | null] for operations that can fail.
  • Use explicit error checks at call sites.
  • Reserve panic() and recover() for exceptional or unrecoverable situations, not ordinary failures.
  • Prefer wrapping or returning errors explicitly rather than relying on exception-like control flow.

Common Error-Return Pattern

typescript
const [data, err] = loadConfig(path);
if (err != null) {
  return [zero<Result>(), err];
}
return [data, null];

Ignoring Values Intentionally

When only one side of the tuple matters, keep the ignored value obvious:

typescript
const [_data, err] = loadConfig(path);
if (err != null) {
  return err;
}
return null;
typescript
const [value, _err] = maybeReadCached(path);
return value;

If the project has a stronger local convention, follow that convention. Otherwise prefer explicit placeholder names like _err or _value.

Presence and Comma-Ok Style Results

Not every tuple return is an error return. Some APIs return a value plus a presence flag:

typescript
const [value, ok] = cache.Get(key);
if (!ok) {
  return [zero<Item>(), new Error("missing item")];
}
return [value, null];

Treat these as Go-style multi-return APIs, not as JavaScript destructuring sugar.

Error Construction

typescript
import { New } from "errors";

return [zero<Result>(), New("missing config")];
typescript
import { Errorf } from "fmt";

return [zero<Result>(), Errorf("invalid config %s: %v", name, err)];

Practical Guidance

  • Destructure close to the call site when the returned values are used immediately.
  • If the result needs to be passed through multiple branches, introduce named locals instead of repeatedly re-calling the function.
  • Prefer explicit branch flow over compressing everything into nested conditional expressions.

Common Mistakes

  • Treating [T, error | null] as if it were a Promise result.
  • Translating try/catch directly instead of redesigning the function contract.
  • Searching the codebase for "special syntax" before trying ordinary tuple destructuring.
  • Hiding ignored values so aggressively that the control flow becomes less clear.
  • Mixing error-return tuples with exception-style control flow in the same path.
  • Using panic() for everyday business errors.

Ok-chain for sequential fallible operations

ok() is a builtin that wraps a [T, error | null] result into a chainable pipeline, avoiding nested if-err checks:

typescript
// Instead of nested if-err:
function loadNumber(path: string): [int, error | null] {
  return ok(readLine(path))
    .flatMap<int>(s => Atoi(s))
    .unwrap();
}

Chain methods (non-terminal): map, flatMap, mapErr, orElse, tap, tapErr Terminal methods (end the chain): unwrap, must, orDefault, orZero, fold

typescript
// mapErr to wrap errors with context
ok(loadConfig(path))
  .mapErr(err => Errorf("config failed: %v", err))
  .unwrap();

// orDefault to provide a fallback value
const settings = ok(loadConfig(path))
  .map(config => parseSettings(config))
  .orDefault(defaultSettings);

Error wrapping with context

typescript
import { Errorf } from "fmt";

const [user, err] = findUser(id);
if (err != null) {
  return [zero<Response>(), Errorf("findUser(%d): %v", id, err)];
}