Question about nested try catch statements and error handling

Viewed 124

I am dealing with multiple nested try catch statements due to different wrappers and things passed to one another in a pipeline.

I am trying to get test coverage on a catch block for a function with the name of put by mocking the response and throwing an error when data is eventually passed somewhere invoking .json() on it and throwing an error of the shape defined below:

it('hits the catch', async () => {
        fetch.mockImplementation(() =>
          Promise.resolve({
            ok: true,
            json: () => {
              throw { type: 'error', message: 'hit the catch' }
            },
          })
        )
    
        expect(
          await put({
            path: '/script/1',
            body: {
              name: 'newName',
            },
          })
        ).toThrow({
          message: 'hit the catch',
          type: 'error',
        })
      })

This function in turn calls a client function:

export const put = async <I, R>({ path, body }: { path: string; body: I }) => {
  try {
    return client<I>(getServiceUrl(path), 'PUT', getServiceHeaders(token), body)
  } catch (e) {
    throw handleClientError(e)
    // return handleClientError(e)
  }
}

The client function is a wrapper for fetch:

    export const client = async <I>(
      url: string,
      method: 'POST' | 'GET' | 'PUT' | 'PATCH' | 'DELETE',
      headers: HeadersInit,
      body?: I
    ): Promise<ServiceResonse<I>> => {
      try {
        const res: Response = await fetch(url, {
          method,
          headers,
          body: JSON.stringify(body),
        })
    
        return getServiceResponse(res)
      } catch (e) {
        throw handleClientError(e)
        // return handleClientError(e)
      }
}

And here are the various helper functions called as well:

export const getServiceHeaders = (token: string = ''): HeadersInit => {
  try {
    if (token) {
      return {
        'Content-Type': 'application/json',
        Authorization: 'Bearer ' + token,
      }
    } else {
      return {
        'Content-Type': 'application/json',
      }
    }
  } catch (e) {
    const msg = 'Error getting service headers ' + JSON.stringify(e)
    console.error(msg) //shouldn't ever happen
    throw new Error(msg)
  }
}

export const getServiceResponse = async <T>(
  response: Response
): Promise<ServiceResonse<T>> => {
  try {
    const data = await response.json()
    if (!response.ok) {
      let error = {
        code: (data && data.code) || 'error',
        message:
          (data && data.message) ||
          response.statusText ||
          'Unkown Error x03391aa00',
        invalid: null,
      }

      switch (response.status) {
        case 400:
          if (error.code == 'invalid') {
            return (error.invalid = ((data && data.errors) || []).reduce(
              (items: RequestErrorResponseFields, i: APIResponseErrorItem) => {
                items[i.field] = i.message
                return items
              },
              {}
            ))
          }

          break
        case 401:
          // TODO handle error)
          return { type: 'error', fields: {}, message: 'Invalid Token' }
          break
      }
      return Promise.reject(error)
    } else {
      return data
    }
  } catch (e) {
    throw e
  }
}

export const getServiceUrl = (
  path: string,
  options: { [k: string]: string | number } = {}
): string => {
  if (typeof path !== 'string') throw new Error('path must be of type string')
  if (typeof options !== 'object')
    throw new Error('options must be of type object')

  const query = Object.keys(options)
    .map(k => k + '=' + options[k])
    .join('&')

  //todo move to general configuration file, still use process.env
  const baseUrl = process.env.API_URL || 'http://localhost/ipa'
  return `${baseUrl}${path}${query.length ? '?' + query : ''}`
}

export const handleClientError = (
  err: { message?: string } | RequestErrorResponse
) => {
  if ('type' in err) {
    throw err
    // return Promise.reject(err)
  } else {
    throw {
      code: 'error',
      message: err.message || 'Unkown - 0x39aef3991',
      errors: [],
    }
    // return Promise.reject({
    //   code: 'error',
    //   message: err.message || 'Unkown - 0x39aef3991',
    //   errors: [],
    // })
  }
}

I am trying to get testing coverage on the catch block in the put function by forcing an exception and passing it up to the catch but I can't seem to get it to reach. I am not sure at what point to force and exception and then how to pass it up and test against it. Does anyone have any ideas on how I should approach this or what I am missing?

1 Answers

In your async put function, you have a line that returns a Promise:

return client(getServiceUrl(path), 'PUT', getServiceHeaders(token), body)

The try/catch block that you have around this line will catch synchronously thrown errors, but not async ones. If you want to use a try/catch block to catch those asynchronous errors, you'll need to add an await statement:

export const put = async <I, R>({ path, body }: { path: string; body: I }) => {
  try {
    return await client<I>(getServiceUrl(path), 'PUT', getServiceHeaders(token), body)
  } catch (e) {
    throw handleClientError(e)
    // return handleClientError(e)
  }
}

Alternatively, you could use a regular promise catch:

return await client<I>(getServiceUrl(path), 'PUT', getServiceHeaders(token), body).catch(handleClientError)

(note: I didn't fully look through the code -- there might be more spots in there with async issues)

Related