Pull to refresh
92.51
Skillfactory
Онлайн-школа IT-профессий

Лучшая практика обработки ошибок в современном JavaScript

Reading time6 min
Views11K
Original author: Christopher Tran

Когда вы пишете код, важно учитывать ситуации, приводящие к ошибкам. Обработка ошибок — это неотъемлемая часть работы над веб-приложением. Мы посмотрим на некоторые рекомендации по обработке ошибок в JavaScript. Чтобы не тратить ваше время зря, сразу поясняем, что описанное в статье может быть не в новинку многопытным кодерам. Если вы себя таким считаете — смело пропускайте этот материал, всех остальных приглашаем под кат.



Расширяем класс Error


Часто бывает полезно предоставить детальное описание ошибки внутри обработчика. И под этим я подразумеваю не только четкое сообщения об ошибке. Я имею в виду расширение класса Error. Расширив класс Error, вы можете настроить полезные при отладке свойства name и message, а также написать пользовательские геттеры, сеттеры и другие методы:

class BadParametersError extends Error {
  name = 'BadParametersError'
  constructor(message) {
    super(message)
  }
  get recommendation() {
    return this._recommendation
  }
  set recommendation(recommendation) {
    this._recommendation = recommendation
  }
}

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

Посмотрим на ситуацию, когда расширение Error приносит пользу. Допустим, у вас есть функция, принимающая список функций решателя. Она принимает аргумент, проходит по списку решателей и передает аргумент в каждую функцию. Если функция возвращает какой-то результат, проход останавливается и этот результат возвращается функцией:

// Takes a list of resolvers, composes them and returns a func that calls
// each resolvers on the provided args.
function composeResolvers(...resolvers) {
  return (args) => {
    let result
    for (let index = 0; index < resolvers.length; index++) {
      const resolve = resolvers[index]
      result = resolve(args)
      if (result) {
        break // Abort the loop since we now found a value
      }
    }
    return result
  }
}

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

import composeResolvers from '../composeResolvers'
const resolvers = []
const someResolverFn = (userInput) => {
  if (userInput > 2002) {
    return 'NewKidsOnTheBlock'
  }
  return 'OldEnoughToVote'
}
// Pretending our code is only stable/supported by certain browsers
if (/chrome/i.test(navigator.userAgent)) {
  resolvers.push(someResolverFn)
}
const resolve = composeResolvers(...resolvers)
window.addEventListener('load', () => {
  const userInput = window.prompt('What year was your computer created?')
  const result = resolve(userInput)
  window.alert(`We recommend that you register for the group: ${result}`)
})

Когда пользователь нажимает OK, его возраст присваивается userInput и передается в качестве аргумента функции из composeResolvers:

import composeResolvers from '../composeResolvers'
const resolvers = []
const someResolverFn = (userInput) => {
  if (userInput > 2002) {
    return 'NewKidsOnTheBlock'
  }
  return 'OldEnoughToVote'
}
// Pretending our code is only stable/supported by certain browsers
if (/chrome/i.test(navigator.userAgent)) {
  resolvers.push(someResolverFn)
}
const resolve = composeResolvers(...resolvers)
window.addEventListener('load', () => {
  const userInput = window.prompt('What year was your computer created?')
  const result = resolve(userInput)
  window.alert(`We recommend that you register for the group: ${result}`)
})



По окончании работы запускается window.alert, чтобы показать пользователю его группу:



Код работает нормально. Но что, если пользователь смотрит страницу не в Chrome? Тогда строка resolvers.push(someResolverFn) не работает. Ниже мы видим неприятный результат:



Мы можем предупредить необработанные ошибки, бросив обычную Error, или можем использовать более подходящую BadParametersError:

// Takes a list of resolvers, composes them and returns a func that calls
// each resolvers on the provided args.
function composeResolvers(...resolvers) {
  if (!resolvers.length) {
    const err = new BadParametersError(
      'Need at least one function to compose resolvers',
    )
    err.recommendation =
      'Provide a function that takes one argument and returns a value'
    throw err
  }
  return (args) => {
    let result
    for (let index = 0; index < resolvers.length; index++) {
      const resolve = resolvers[index]
      result = resolve(args)
      if (result) {
        break // Abort the loop since we now found a value
      }
    }
    return result
  }
}

Так у ошибки гораздо меньше шансов попасть к пользователю. Сообщение заставляет разработчика исправить ситуацию:



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

const resolve = composeResolvers(...resolvers)
window.addEventListener('load', () => {
  const userInput = window.prompt('What year was your computer brought to you?')
  let result
  try {
    result = resolve(userInput)
  } catch (error) {
    if (error instanceof BadParametersError) {
      console.error(
        `[Error] ${error.message}. Here's a recommendation: ${error.recommendation}`,
      )
      console.log(error.recommendation)
    } else {
      // Do some fallback logic
      return window.alert(
        'We are sorry, there was a technical problem. Please come back later',
      )
    }
  }
  window.alert(`We recommend that you register for the group: ${result}`)
})

Применение TypeError


Мы часто работаем с Error, но когда есть более подходящая встроенная ошибка, полезно не пренебрегать ей:

async function fetchDogs(id) {
  let result
  if (typeof id === 'string') {
    result = await api.fetchDogs(id)
  } else if (typeof id === 'array') {
    result = await Promise.all(id.map((str) => api.fetchDogs(id)))
  } else {
    throw new TypeError(
      'callSomeApi only accepts a string or an array of strings',
    )
  }
  return result
}
const params = { id: 'doggie123' }
let dogs
fetchDogs(params)
  .then((dogs) => {
    dogs = dogs
  })
  .catch((err) => {
    if (err instanceof TypeError) {
      dogs = Promise.resolve(fetchDogs(params.id))
    } else {
      throw err
    }
  })


Тестирование


Благодаря наследованию Error тестирование становится надежнее. Такие ошибки можно использовать при написании ассертов:

import { expect } from 'chai'
import chaiAsPromised from 'chai-as-promised'
import fetchCats from '../fetchCats'
chai.use(chaiAsPromised)
it('should only take in arrays', () => {
  expect(fetchCats('abc123')).to.eventually.rejectWith(TypeError)
})


Важно не перестараться


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

class AbortExecuteError extends Error {
  name = 'AbortExecuteError'
  constructor(message) {
    super(message)
  }
}
class BadParameters extends Error {
  name = 'BadParameters'
  constructor(message) {
    super(message)
  }
}
class TimedOutError extends Error {
  name = 'TimedOutError'
  constructor(message) {
    super(message)
  }
}
class ArrayTooLongError extends Error {
  name = 'ArrayTooLongError'
  constructor(message) {
    super(message)
  }
}
class UsernameError extends Error {
  name = 'UsernameError'
  constructor(message) {
    super(message)
  }
}

Тогда вы переосмысливаете подход и стараетесь понять, действительно ли вам нужна такая обработка ошибок. В большинстве случаев достаточно четко сообщить об ошибке. Ассерт приносит максимальную выгоду только в том случае, когда необходимо добавить контекст. Например, токен, чтобы повторить запрос по тайм-ауту:

class TimedOutError extends Error {
  name = 'TimedOutError'
  retried = 0
  constructor(message) {
    super(message)
  }
  set retry(callback) {
    this._retry = callback
  }
  retry(...args) {
    this.retried++
    return this._retry(...args)
  }
}
class ConnectToRoomTimedOutError extends TimedOutError {
  name = 'ConnectToRoomTimedOutError'
  constructor(message) {
    super(message)
  }
  get token() {
    return this._token
  }
  set token(token) {
    this._token = token
  }
}
let timeoutRef
async function connect(token) {
  if (timeoutRef) clearTimeout(timeoutRef)
    timeoutRef = setTimeout(() => {
      const err = new ConnectToRoomTimedOutError(
        'Did not receive a response from the server',
      )
      err.retry = connect
      err.token = token
      throw err
    }, 10000)
    const room = await api.join(token)
    clearTimeout(timeoutRef)
    return room
  }
}
const joinRoom = () => getToken().then((token) => connect(token))
async function start() {
  try {
    let room = await joinRoom()
    return room
  } catch (err) {
    if (err instanceof ConnectToRoomTimedOutError) {
      try {
        // Lets retry one more time
        room = await err.retry(err.token)
        return room
      } catch (innerErr) {
        throw innerError
      }
    }
    throw err
  }
}
start()
  .then((room) => {
    console.log(`Received room, oh yea!`, room)
  })
  .catch(console.error)

Помните, что обработка ошибок экономит ваши деньги и время.

image

Получить востребованную профессию с нуля или Level Up по навыкам и зарплате, можно, пройдя онлайн-курсы SkillFactory:



Tags:
Hubs:
+8
Comments5

Articles

Information

Website
www.skillfactory.ru
Registered
Founded
Employees
501–1,000 employees
Location
Россия
Representative
Skillfactory School