OxL Справочник OxL
РусскийEnglish

Паттерны ошибок

Ошибка как enum#

Задача. Описать несколько причин отказа одним типом и разобрать их в одном match.

oxl✓ проверяется в CI
@error enum PayError {
  Declined(reason: str)
  Fraud
  LimitExceeded(max: i64)
}

fn charge(amount: i64) -> i64 | PayError {
  if amount > 1000 { fail PayError.LimitExceeded(1000) }
  if amount == 13 { fail PayError.Fraud }
  if amount < 0 { fail PayError.Declined("negative amount") }
  amount
}

fn describe(r: i64 | PayError) -> str =>
  match r {
    i64 n => "charged {n}"
    PayError.Declined(reason) => "declined: {reason}"  // варианты enum-ошибки разбираются как обычные
    PayError.Fraud => "fraud suspected"
    PayError.LimitExceeded(max) => "over the limit of {max}"
  }

fn main() io {
  for a in [50, 13, -5, 5000] { print(describe(charge(a))) }
}
Вывод
charged 50
fraud suspected
declined: negative amount
over the limit of 1000
  • @error enum PayError { Declined(reason: str); Fraud; LimitExceeded(max: i64) } — одна «семья» ошибок; варианты несут данные.
  • Функция возвращает i64 | PayError, а match разбирает и успех, и каждый вариант. Забытый вариант — non-exhaustive.
  • Когда использовать enum, а когда несколько @error data: enum — если вызывающий всегда обрабатывает все причины вместе; отдельные типы — если разные вызывающие интересуются разными ошибками и объединение растёт по цепочке вызовов.

Повтор с задержкой#

Задача. Повторить нестабильную операцию несколько раз, не повторяя бесконечно.

oxl✓ проверяется в CI
@error data Flaky { attempt: i32 }

fn unreliable(attempt: i32) -> str | Flaky {
  if attempt < 3 { fail Flaky{ attempt } }
  "ok on attempt {attempt}"
}

fn withRetry(max: i32) -> str | Flaky  io, time {
  var attempt = 1
  loop {
    match unreliable(attempt) {
      str s => break s
      Flaky e => {
        if attempt >= max { fail e }  // попытки кончились — отдаём последнюю ошибку
        print("attempt {attempt} failed, retrying")
        sleep(Duration.ofMillis(10 * attempt as i64))  // линейная задержка между попытками
        attempt += 1
      }
    }
  }
}

fn main() io, time {
  match withRetry(5) {
    str s => print(s)
    Flaky e => print("gave up after {e.attempt}")
  }
  match withRetry(2) {
    str s => print(s)
    Flaky e => print("gave up after {e.attempt}")
  }
}
Вывод
attempt 1 failed, retrying
attempt 2 failed, retrying
ok on attempt 3
attempt 1 failed, retrying
gave up after 2
  • loop { … break value } — цикл-выражение: break s отдаёт успешное значение, fail e — последнюю ошибку, когда попытки кончились.
  • Задержка растёт с номером попытки (10 * attempt мс); для сетевых rpc то же делают декларативно атрибутами @retry(n, backoff: …) — но только для @idempotent и @readonly методов, чтобы повтор не выполнил действие дважды.
  • Подводный камень: повторяйте только временные ошибки (Timeout, Unavailable). Ошибку «неверный ввод» повтор не исправит.

Запасной путь#

Задача. Если значения нет в кэше — взять из базы; если база недоступна — вернуть ошибку.

oxl✓ проверяется в CI
@error data CacheMiss { key: str }
@error data DbDown { why: str }

fn fromCache(key: str) -> str | CacheMiss {
  if key == "a" { return "cached-a" }
  fail CacheMiss{ key }
}

fn fromDb(key: str) -> str | DbDown {
  if key == "c" { fail DbDown{ why: "connection refused" } }
  "db-{key}"
}

fn load(key: str) -> str | DbDown {
  await fromCache(key)
    !> CacheMiss => fromDb(key)?  // !> заменяет ошибку запасным путём
}

fn main() io {
  for k in ["a", "b", "c"] {
    match load(k) {
      str s => print("{k}: {s}")
      DbDown e => print("{k}: db down ({e.why})")
    }
  }
}
Вывод
a: cached-a
b: db-b
c: db down (connection refused)
  • await fromCache(key) !> CacheMiss => fromDb(key)? — обработчик !> срабатывает только на CacheMiss; другие ошибки (здесь DbDown) проходят дальше по цепочке.
  • Тип результата load — str | DbDown: CacheMiss из него исчез, потому что обработан. Компилятор это проверяет.
  • Несколько обработчиков можно ставить подряд: !> A => … !> B => ….

Освобождение ресурсов: defer#

Задача. Гарантированно закрыть ресурс, как бы ни завершилась функция.

oxl✓ проверяется в CI
fn process(name: str) -> str | Failed  io {
  print("open {name}")
  defer print("close {name}")  // defer выполняется при любом выходе из функции
  if name == "bad" { fail Failed{} }
  print("work {name}")
  "done {name}"
}

@error data Failed {}

fn main() io {
  for n in ["good", "bad"] {
    match process(n) {
      str s => print(s)
      Failed => print("failed {n}")
    }
  }
}
Вывод
open good
work good
close good
done good
open bad
close bad
failed bad
  • defer выражение выполняется при любом выходе из функции: нормальном, через fail, через ? и даже при панике.
  • Несколько defer выполняются в обратном порядке (последний — первым), как стек вызовов.
  • Типы с @must_close (Server, WebSocket, …) требуют закрытия: компилятор сообщает must-close, если вы забыли defer x.close().

Вернуться: кулинарная книга.

См. также

OxL 1.0.0-beta · Версия документации 1.0.0-beta© OxwaldLink · сгенерировано из Стандарта, исходников std и руководства