unjs / ofetch

😱 A better fetch API. Works on node, browser and workers.
MIT License
3.98k stars 125 forks source link

Go-like syntax to handle errors #370

Open enkot opened 6 months ago

enkot commented 6 months ago

Describe the feature

I suggest to add throw: boolean option so we can return the HTTP error instead of throwing it. This way users can get full type safety after implementation of Typed API definition. Example:

const { data, error } = await $fetch('/api/v1', { throw: false })

or not throw by default:

const $api = $fetch.create<unknown, ApiDefinition>({
  baseURL: 'http://httpstat.us',
  throw: false
})

const { data, error } = await $api('/500')

error // -> error is typed here based on the ApiDefinition

data - 2xx response if OK, otherwise undefined error - 5xx, 4xx response if not OK, otherwise undefined

Alternatives

Return array:

const [data, error] = $fetch('/api/v1', { throw: false })

Additional information

pi0 commented 6 months ago

I like the idea. Only thinking it might be more suitable for $fetch.raw where we return full object (data body, headers and now possibly new headers). Combining two different return values for $fetch can increase the types complexity at very least but open to ideas.

anuragkumar19 commented 6 months ago

I fully agree with @pi0.

@enkot, Generally APIs ErrorResponse schema is same for every routes. So, another way to achieve it would be we expose ErrorResponse interface which will be used by raw to override the response type with ErrorResponse if success is false. ErrorResponse can overwritten via .d.ts file.

Currently the work around will be to let your api return status_code:number or success:boolean & make response a union and use it with ignoreResponseError to get correct typing.

johannschopplich commented 6 months ago

By the way, you could support both syntaxes in the same return object:

const { data, error } = $fetch('/api/v1', { throw: false })
// And:
const [data, error] = $fetch('/api/v1', { throw: false })

Implementation:

export function createIsomorphicDestructurable<
  T extends Record<string, unknown>,
  A extends readonly any[],
>(obj: T, arr: A): T & A {
  const clone = { ...obj }

  Object.defineProperty(clone, Symbol.iterator, {
    enumerable: false,
    value() {
      let index = 0
      return {
        next: () => ({
          value: arr[index++],
          done: index > arr.length,
        }),
      }
    },
  })

  return clone as T & A
}

This idea originated from @antfu.

enkot commented 6 months ago

Good point 👍 One more reference: https://vueuse.org/shared/makeDestructurable/#makedestructurable

johannschopplich commented 6 months ago

@enkot Absolutely! It's the same implementation under the hood. The VueUse one also has IE11 support, probably because VueUse still supports Vue 2. :)

enkot commented 6 months ago

@enkot, Generally APIs ErrorResponse schema is same for every routes. So, another way to achieve it would be we expose ErrorResponse interface which will be used by raw to override the response type with ErrorResponse if success is false. ErrorResponse can overwritten via .d.ts file.

Agree, generally the same for all routes, but not 100%. There are situations when, for example, the project uses gateways (aggregators) to different APIs and error response can be quite different. An error response is just a different type of response (not 200), so why should it be treated differently in terms of types?