Как читать сообщения об ошибках из объекта ошибок javascript

#javascript

Вопрос:

Не мог бы кто-нибудь помочь мне в нижеследующем вопросе, пожалуйста: -)

Я делаю почтовый вызов с помощью действия redux, которое приведено ниже.

 export const addEmployee = ({ firstName, surname, contactNumber, email }) => async dispatch => {
const payloadBody = JSON.stringify({ firstName, surname, contactNumber, email });
    fetch('/api/users', { 
            method: 'POST', 
            body: payloadBody,
            headers: {
                'Content-Type': 'application/json'
            }
        })
        .then(response => {
            if (!response.ok) {
                return response.text()
                .then(text => { 
                    throw Error(text)
                });
            } else {
                dispatch(setAlert("New Employee added ", 'danger'));
            }
        })
        .catch(error => {
            console.log('>>> in CATCH block, error is =>', error);
            console.log('>>> in CATCH block, error name is =>', error.name);
            console.log('>>> in CATCH block, error message is =>', error.message);

            let allKeys = Object.getOwnPropertyNames(error);
            console.log(allKeys); 

            // const errors = [];
            // Object.keys(error.message).forEach(key => {
            //     console.log('>>> key are ', key)
            // })


            // const keys = Object.keys(error.message);
            // console.log(keys);

            // const errors = error.message['errors'];
            
            
            // const errors = error.response.data.errors;

            // if (errors) {
            //     errors.forEach(error => dispatch(setAlert(error.msg, 'danger')));
            // }

            dispatch({
                type: REGISTER_FAIL
            });
        })
}
 

Выше сообщение о вызове при сбое возвращает тело с сообщением об ошибке, пример приведен ниже

     {
    "errors": [
        {
            "msg": "User already exist with email"
        }
     ]
}
 

Вопрос
Чего я пытаюсь добиться, так это захватить errors[] и передать сообщение об ошибке компоненту, проблема, с которой я сталкиваюсь, заключается в доступе к error[] массиву в возвращаемом сообщении массива. Ниже я опишу, что я пытался, это также можно увидеть в методе действий redux, который я опубликовал выше.

Попробуйте-1
console.log('>>> in CATCH block, error is =>', error); дает только Error

Попробуйте-2
console.log('>>> in CATCH block, error name is =>', error.name); {"errors":[{"msg":"User already exist with email"}]} , и тип этого string , так как я возвращаю текст() return response.text().then(text => { throw Error(text) })

Попробуй-3

Когда я возвращаюсь как json() return response.json().then(text => { throw Error(text) }) и console.log('>>> in CATCH block, error message is =>', error.message); получаю объект.

Снова возникают вопросы, чего я пытаюсь достичь, это захватить errors[] и передать сообщение об ошибке компоненту, такому как показано ниже

             const errors = error.message; // this is where I'd like to extract the error.

             if (errors) {
                 errors.forEach(error => dispatch(setAlert(error.msg, 'danger')));
             }
 

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

Комментарии:

1. если error.name является строкой , вы не можете получить ключ , попробуйте проанализировать объект с помощью JSON.parse(error.name) , чтобы вы могли получить ключ ошибок JSON.parse(error.name)["errors"]

2. @EkaCipta большое спасибо, приятель, это сработало. Кстати error.name , то, что я выбрал только имя, однако, error.message сделало свое дело. Эта строка извлекает массив ошибок из тела ответа JSON.parse(error.message).errors Еще раз Большое спасибо 🙂

Ответ №1:

Шаблон для вывода ошибок, восстановленных из полезной нагрузки HTTP стандартного формата

Ваше действие redux действительно работает по протоколу HTTP. Иногда сервер отвечает плохими новостями, и кажется, что существует стандартизированный формат, который сервер использует для сообщения этих новостей. Кроме того, иногда ваш собственный код выдает. Вы хотите решить оба вида проблем с управляющими структурами, связанными с Error s.

Базовый шаблон для асинхронного действия восстановления

Прежде чем мы начнем: ваше действие отмечено async , но вы все еще цепляетесь .then и .catch . Давайте переключимся на асинхронность/ожидание, преобразуя это:

 export const addEmployee = (/*...*/) = async ( dispatch, getState ) => {
  fetch(/* ... */)
  .then(response => {
    return response.text()
    .then(text => { 
      // happy-path logic
      throw Error(text)
    })
  })
  .catch(error => {
    // sad-path logic
    dispatch(/* ... */)
  })
}
 

…в это:

 export const addEmployee = (/*...*/) = async ( dispatch, getState ) => {
  try {
    let response = await fetch(/* ... */)
    let responseText = await response.text()
    // happy-path logic
    dispatch(/* ... */)
    return // a redux action should return something meaningful
    
  } catch ( error ) {
    // sad-path logic
    dispatch(/* ... */)
    return // a failed redux action should also return something meaningful
  }
}
 

Теперь давайте поговорим об ошибках.

Error basics

Meet throw :

 try { throw 'mud'      } catch( exception ) { /* exception === 'mud' */ }
try { throw 5          } catch( exception ) { /* exception === 5     */ }
try { throw new Date() } catch( exception ) { /* exception is a Date */ }
 

You can throw just about anything. When you do, execution halts and immediately jumps to the closest catch , searching all the way through the stack until it finds one or runs out of stack. Wherever it lands, the value you provided to throw becomes the argument received by catch (known as an «exception»). If nothing catches it, your JS console logs it as an «uncaught exception.»

You can throw anything, but what should you throw? I think you should only throw instances of Error , or one of its subclasses. The two main reasons are that the Error class does some helpful things (like capturing a stacktrace), and because one of your two sources of failure is already going to be throwing Error instances, so you must do something similar if you wish to handle both with a single codepath.

Meet Error :

 try {
  throw new Error('bad news')
  
} catch ( error ) {
  console.log(error.message)
  //> 'bad news'
}
 

We already know that an Error will be thrown if code within your action blows up, e.g. JSON.parse fails on the response body, So we don’t have to do anything special to direct execution onto the catch path in those scenarios.

The only thing we have to be responsible for is to check whether the HTTP response contains something that looks like your server’s «standard error payload» (more on that later), which your sample suggests is this:

 {
  "errors": [
    {
      "msg": "ERROR CONTENT HERE"
    }
  ]
}
 

Here’s the core issue

This handling has to be special because no javascript engine considers it an error simply to receive an HTTP payload that can be parsed as JSON and which contains a key named «errors». (Nor should they.) This payload pattern is merely a custom convention used by some or all of the HTTP endpoints that you talk to.

That’s not to say it’s a bad idea. (I think it’s great!) But that explains why it must be done custom: because this pattern is just your private little thing, and not actually special in a way that would make browsers treat it the special way you want.

So here’s our plan:

  1. make the request, relying on try/catch to capture things thrown by our tools
    • if we get a response that seems bad:
      1. examine the payload for an error encoded in the «standard format»; I call anything like this an «API error»
      2. if we find an API error, we will create and throw our own Error , using the API error content as its message
      3. if we don’t find an API error, we’ll treat the raw body text of the response as the error message
    • if we get a response that seems good:
      1. dispatch the good news (and useful data) to the store

Here’s what that looks like in code:

 export const addEmployee = ({
  firstName,
  surname,
  contactNumber,
  email
}) => async ( dispatch, getState ) => {
  const payloadBody = {
    firstName,
    surname,
    contactNumber,
    email
  }
  
  try {
    // step 1
    let response = await fetch('/api/users', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify(payloadBody)
    })
    
    let responseText = await response.text()
    if (!response.ok) {
      // step 2
      let errorString = getErrorMessageFromResponseBody(responseText)
      throw new Error(errorString) // API errors get thrown here
    }
    
    // step 3
    let responseJson = JSON.parse(responseText)
    
    dispatch(setAlert('New Employee added', responseJson.user.name))
    
    /*
      A redux action should always returns something useful.
      addEmployee might return the `user` object that was created.
    */
    return responseJson.user
    
  } catch ( error ) {
    // all errors land here
    dispatch({
      type: REGISTER_FAIL,
      message: error.message
    })
    /*
      A failed redux action should always return something useful (unless you prefer to throw).
      For now, we'll return the reason for the failure.
    */
    return error.message
  }
}


function getErrorMessageFromResponseBody( string ) {
  let errorString = string
  
  try {
    let json = JSON.parse(string)
    if(json.errors) {
      errorString = json.errors[0].msg
    }
  } catch ( parseOrAccessError ) {}
  
  return errorString
}
 

Вот что можно бросить в этот catch блок:

  • все, что бросается JSON.parse при применении к аргументам
  • что-нибудь брошенное fetch
  • если !response.ok , вся полезная нагрузка ответа (или просто сообщение об ошибке, если полезная нагрузка содержит ошибку API)

Обработка исключений

Как вы можете отличить эти различные виды неудач друг от друга? Два способа:

  1. Некоторые сбои приводят к появлению определенных подклассов Error , которые вы можете проверить с помощью error instanceof SomeErrorClass :
    • JSON.stringify выбрасывает TypeError , если он не может сериализовать свой аргумент (если у вас где-то есть настройки .toJSON , он также может выбросить все, что выбрасывает)
    • fetch бросает TypeError , если он не может выйти в Интернет
    • JSON.parse выдает a SyntaxError , если строка не может быть проанализирована (если вы используете пользовательский оживитель, эти ошибки тоже будут выданы)
  2. У любого экземпляра Error или его подклассов будет a .message ; вы можете проверить эту строку для конкретных случаев

Как вы должны с ними обращаться?

  • Если JSON.stringify что-то взорвется, это потому, что вы неправильно ввели свои данные. В этом случае вы, вероятно, захотите сделать что-то, что предупредит разработчика о том, что что-то сломано, и поможет диагностировать проблему:
    1. console.error(error)
    2. отправьте некоторое действие по устранению сбоя, которое включает в себя error.message
    3. отображение общего сообщения об ошибке на экране
  • Если fetch это произойдет, вы можете отправить сообщение об ошибке, в котором пользователю будет выдано предупреждение «исправьте свой Wi-Fi».
  • Если JSON.parse это произойдет, сервер тает, и вы должны показать общее сообщение об ошибке.

Немного утонченности

Таковы основные механики, но теперь вы сталкиваетесь с запутанной ситуацией. Давайте перечислим некоторые проблемы:

  • Возможно, вы уже заметили одну проблему: «нет интернета» будет отображаться так же, как и «циклические данные»: выброшенный TypeError .
  • Оказывается, точный текст JSON.stringify ошибок зависит от фактического значения, предоставленного этой функции, поэтому вы не можете сделать что-то подобное error.message === CONSTANT_STRINGIFY_ERROR_MESSAGE .
  • Возможно, у вас нет исчерпывающего списка всех msg значений, которые сервер может отправить при ошибке API.

Итак, как вы должны определить разницу между проблемой, о которой сообщает нормальный сервер, и ошибкой на стороне клиента, и сломанным сервером, и непригодными для использования пользовательскими данными?

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

 class APIError extends Error {}

APIError.fromResponseText = function ( responseText ) {
  // TODO: paste entire impl of getErrorMessageFromResponseBody
  let message = getErrorMessageFromResponseBody(responseText)
  return new APIError(message)
}

 

Тогда мы сможем сделать:

 // throwing
if (!response.ok) {
  // step 2
  throw APIError.fromResponseText(responseText)
}

// detecting
catch ( exception ) {
  if(exception instanceof APIError) {
    switch(APIError.message) {
      case 'User already exist with email':
        // special logic
        break
      case 'etc':
        // ...
    }
  }
}
 

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

Сообщения об ошибках для здравомыслящих людей

Считать:

 function add( x, y ) {
  if(typeof x !== 'number')
    throw new Error(x   ' is not a number')
  
  if(typeof y !== 'number')
    throw new Error(y   ' is not a number')
  
  return x   y
}
 

Every time add is called with a different non-numeric x , the error.message will be different:

 add('a', 1)
//> 'a is not a number'

add({ species: 'dog', name: 'Fido' }, 1) 
//> '[object Object] is not a number'
 

The problem in both cases is that I’ve provided an unacceptable value for x , but the messages are different. That makes it unnecessarily hard to group those cases together at runtime. My example even makes it impossible to tell whether it’s x or y that offends!

These troubles apply pretty generally to the errors you’ll receive from native and library code. My advice is to not repeat them in your own code if you can avoid it.

The simplest remedy I’ve found is just to always use static strings for error messages, and put some thought into establishing conventions for yourself. Here’s what I do.

There are generally two kinds of errors:

  • some value I wish to use is objectionable
  • some operation I attempted has failed

In the first case, the relevant info is:

  • which datapoint is bad; I call this the «topic»
  • why it is bad, in one word; I call this the «objection»

All error messages related to objectionable values ought to include both datapoints, and in a manner that is consistent enough to facilitate flow-control while remaining understandable by a human. And ideally you should be able to grep the codebase for the literal message to find every place that can throw the error (this helps enormously with maintenance).

Here is how I construct the messages:

 [objection] [topic]
 

There is usually a discrete set of objections:

  • missing: value was not supplied
  • unknown: could not find value in DB amp; other «bad key» issues
  • unavailable: value is already taken (e.g. username)
  • forbidden: sometimes specific values are off-limits despite being otherwise fine (e.g. no user may have username «root»)
  • invalid: heavily overused by dev community; treat as option of last resort; reserved exclusively for values that are of the wrong datatype or syntactically unacceptable (e.g. zipCode = '__!!@' )

I supplement individual apps with more specialized objections as needed, but this set comes up in just about everything.

The topic is almost always the literal variable name as it appears within the code block that threw. To assist with debugging, I think it is very important not to transform the variable name in any way.

This system yields error messages like these:

 'missing lastName'
'unknown userId'
'unavailable player_color'
'forbidden emailAddress'
'invalid x'
 

In the second case, for failed operations, there’s usually just one datapoint: the name of the operation (plus the fact that it failed). I use this format:

 [operation] failed
 

As a rule, operation is the routine exactly as invoked:

 try {
  await API.updateUserProfile(newData)
  
} catch( error ) {
  // can fail if service is down
  if(error instanceof TypeError)
    throw new Error('API.updateUserProfile failed')
}
 

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

Обработка несоответствий сервера

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

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

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

Способ поддержки различных конвертов заключается в добавлении дополнительного кода getErrorMessageFromResponseBody для каждого из различных конвертов:

 function getErrorMessageFromResponseBody( string ) {
  let errorString = string
  
  /*
    "Format A"
    { errors: [{ msg: 'MESSAGE' }] }
    used by most endpoints
  */
  try { /*... */ } catch ( parseOrAccessError ) {}
  
  /*
    "Format B"
    { error: { message: 'MESSAGE' } }
    used by legacy TPS endpoint
  */
  try { /*... */ } catch ( parseOrAccessError ) {}
  
  /*
    "Format C"
    { e: CODE }
    used by bandwidth-limited vendor X
    use lookup table to convert CODE to a readable string
  */
  try { /*... */ } catch ( parseOrAccessError ) {}
  
  return errorString
}

 

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