Как правильно обрабатывать ошибки с IcedCoffeeScript?

В node.js принято возвращать сообщение об ошибке в качестве первого аргумента функции обратного вызова. Существует ряд решений этой проблемы в чистом JS (Promise, Step, seq и т. Д.), Но ни одно из них, похоже, не интегрируется с ICS. Какое будет правильное решение для обработки ошибок без потери читабельности?

Например:

# makes code hard to read and encourage duplication
await socket.get 'image id', defer err, id
if err # ...
await Image.findById id, defer err, image
if err # ...
await check_permissions user, image, defer err, permitted
if err # ...


# will only handle the last error
await  
  socket.get 'image id', defer err, id
  Image.findById id, defer err, image
  check_permissions user, image, defer err, permitted

if err  # ...


# ugly, makes code more rigid
# no way to prevent execution of commands if the first one failed
await  
  socket.get 'image id', defer err1, id
  Image.findById id, defer err2, image
  check_permissions user, image, defer err3, permitted

if err1 || err2 || err3  # ...

2 ответа

Решение

Я решаю эту проблему через стиль и соглашение о кодировании. И это происходит все время. Давайте возьмем ваш фрагмент ниже, выделенный немного больше, чтобы у нас была работоспособная функция.

my_fn = (cb) ->
  await socket.get 'image id', defer err, id
  if err then return cb err, null
  await Image.findById id, defer err, image
  if err then return cb err, null
  await check_permissions user, image, defer err, permitted
  if err then return cb err, null
  cb err, image

Вы совершенно правы, это уродливо, потому что вы во многих местах замыкаете код, и вам нужно помнить, что каждый раз, когда вы возвращаетесь, вызываете cb.

Другие фрагменты, которые вы дали, дают неверные результаты, так как они будут вводить параллелизм там, где требуется сериализация.

Мои личные соглашения по кодированию ICS: (1) возвращать только один раз из функции (контроль которой не выполняется); и (2) попытаться обработать все ошибки на одном уровне отступа. Переписав то, что у вас есть, в моем любимом стиле:

my_fn = (cb) ->
  await socket.get 'image id', defer err, id 
  await Image.findById id, defer err, image                   unless err?
  await check_permissions user, image, defer err, permitted   unless err?
  cb err, image

В случае ошибки в вызове socket.get вам нужно проверить ошибку дважды, и она, очевидно, завершится ошибкой оба раза. Я не думаю, что это конец света, так как это делает код чище.

Кроме того, вы можете сделать это:

my_fn = (autocb) ->
  await socket.get 'image id', defer err, id
  if err then return [ err, null ]
  await Image.findById id, defer err, image
  if err then return [ err, null ]
  await check_permissions user, image, defer err, permitted
  return [ err, image ]

Если вы используете autocb, который не является моей любимой функцией ICS, то компилятор будет вызывать autocb для вас всякий раз, когда вы возвращаете / замыкаете выход из функции. Я считаю, что эта конструкция более подвержена ошибкам из опыта. Например, представьте, что вам нужно получить блокировку в начале функции, теперь вам нужно снять ее n раз. Другие могут не согласиться.

Еще одна заметка, указанная ниже в комментариях. autocb работает как return в том, что он принимает только одно значение. Если вы хотите вернуть несколько значений, как в этом примере, вам нужно вернуть массив или словарь. defer делает деструктивные задания, чтобы помочь вам здесь:

await my_fn defer [err, image]

Как обсуждалось в выпуске № 35 репозитория IcedCoffeeScript, есть еще один метод, основанный на коннекторах в стиле iced, которые представляют собой функции, которые принимают в качестве входных данных обратный вызов / задержку и возвращают другой обратный вызов / задержку.

Представьте, что ваш проект имеет стандартный порядок аргументов для обратных вызовов: первым параметром всегда является ошибка, которая в случае успеха равна нулю. Также предположим далее, что вы хотите оставить функцию при первом признаке ошибки.

Первым шагом является создание соединителя, который я называю "ErrorShortCircuiter" или "ESC":

{make_esc} = require 'iced-error'

Который реализован так:

make_esc = (gcb, desc) -> (lcb) ->
    (err, args...) ->
        if not err? then lcb args...
        else if not gcb.__esc
            gcb.__esc = true
            log.error "In #{desc}: #{err}"
            gcb err

Чтобы увидеть, что это делает, рассмотрим пример того, как его использовать:

my_fn = (gcb) ->
    esc = make_esc gcb, "my_fn"
    await socket.get 'image id', esc defer id
    await Image.findById id, esc defer image
    await check_permissions user, image, esc defer permitted
    gcb null, image

Эта версия my_fn сначала делает ErrorShortCircuiter (или esc), чья работа состоит из двух частей: (1) уволить gcb с объектом ошибки; и (2) записать сообщение о том, где произошла ошибка и что это была за ошибка. Очевидно, вы должны варьировать точное поведение в зависимости от ваших настроек. Затем все последующие вызовы библиотечных функций с обратными вызовами получат обратные вызовы, сгенерированные defer как обычно, а затем запустить через esc разъем, который изменит поведение обратного вызова. Новое поведение заключается в том, чтобы вызвать gcb глобальный для функции на ошибку, и чтобы текущий await блок финиша на успехе. Кроме того, в случае успеха, нет необходимости иметь дело с нулевым объектом ошибки, поэтому только последующие слоты (например, id, image, а также permitted) заполнены.

Эта техника очень мощная и настраиваемая. Основная идея заключается в том, что обратные вызовы, сгенерированные defer действительно являются продолжениями и могут изменить последующий поток управления всей программы. И они могут делать это в библиотеке, так что вы можете получить поведение ошибки, которое вам нужно для многих различных типов приложений, которые вызывают библиотеки с различными соглашениями.

Другие вопросы по тегам