Skip to content

Failures and cleanup — no Effect required

The failure vocabulary is core, and plain Hem already provides structured error handling. error declares a tagged error class:

errors.hemhem
error QuotaExceeded({ orgId: string })
errors.ts — generatedts
class QuotaExceeded extends Error {
  readonly _tag = "QuotaExceeded";
  readonly orgId: string;

  constructor(fields: { readonly orgId: string }) {
    super("QuotaExceeded");
    this.name = "QuotaExceeded";
    this.orgId = fields.orgId;
  }
}

A declared cause or message field forwards to super; export error exports the class. A statement that only drops a value is refused (HEM2018): a bare push(tx, doc) that returns a Promise and is never awaited, or a bare comparison, is almost always a mistake: await it, use the value, or discard it on purpose with let _ = push(tx, doc). That discard emits void (push(tx, doc));, so strict TypeScript and no-floating-Promise rules see the intent without an unused local.

raise throws one (inserting new for locally declared error types), and postfix catch handles: tag arms test with instanceof, a binding arm e => … catches everything, and (the rule that makes it composable) anything unmatched propagates. These are JavaScript exceptions: the error values are typed, but a raise leaves no trace in the function’s type, so callers are not made to handle it. When a failure belongs in the signature, return a Result<T, E> (§19) or move to Effect (§17):

save.hemhem
error SaveFailed({ doc: string })

let save = async (doc: Doc) => {
  use tx = db.begin()                     // released at block exit — even on throw
  await push(tx, doc) catch {
  | QuotaExceeded(_) => cached(doc)
  | _ => raise SaveFailed({ doc: doc.id })
  }
}
save.ts — generatedts
class SaveFailed extends Error {
  readonly _tag = "SaveFailed";
  readonly doc: string;

  constructor(fields: { readonly doc: string }) {
    super("SaveFailed");
    this.name = "SaveFailed";
    this.doc = fields.doc;
  }
}
const save = async (doc: Doc) => {
  using tx = db.begin(); // released at block exit — even on throw
  let _hemValue;
  try {
    _hemValue = await push(tx, doc);
  } catch (error) {
    if (error instanceof QuotaExceeded) {
      _hemValue = cached(doc);
    } else {
      throw new SaveFailed({ doc: doc.id });
    }
  }
  return _hemValue;
};

(An AsyncDisposable transaction would lower to await using; the checker decides per resource.)

use binds a resource: in plain code that means Disposable/AsyncDisposable, lowered to the platform’s using/await using, which Workers RPC stubs and Durable Object handles speak natively. try { } still exists for multi-statement protected regions, but postfix catch is the idiomatic form; it attaches to any expression, await included. In Part II these same keywords change transport (errors travel down a typed channel instead of up the stack, so the types start recording them, and releases survive interruption) without changing a line of what you just learned.

Navigation

Type to search…

↑↓ navigate↵ selectEsc close