nuxt: useAsyncData does not pass errors from server to client-side

Environment

  • Operating System: Linux
  • Node Version: v14.18.1
  • Nuxt Version: 3.0.0-27294839.7e5e50b
  • Package Manager: yarn@1.22.15
  • Bundler: Vite

Reproduction

https://stackblitz.com/edit/github-ygz9df?file=app.vue

Describe the bug

On server side the error content is rendered as expected while the client side says it’s null.

Additional context

No response

Logs

No response

About this issue

  • Original URL
  • State: closed
  • Created 3 years ago
  • Reactions: 6
  • Comments: 26 (5 by maintainers)

Most upvoted comments

@nndnha I understand. One note: if you need to get the status code on client side and don’t mind an extra request, you can also rerun the fetch.

const { error, refresh } = await useFetch('https://jsonplaceholder.typicode.com/404');
if (process.client && error.value) {
  await refresh()
}

Another solution would be to change your backend server to return status 200 instead of 4xx for usage errors, that way you can read the error message/code from the payload and nothing private from the Nuxt server would be exposed.

After a bit of search trough the ohmyfetch (the library used by nuxtjs mentioned in the documentation) I obtained the status code easily, this is my solution:

<script setup lang="ts">

let errorStatus;
const { data: myData, error: error }: { data: any; error?: any } =
  await useFetch(
    "https://jsonplaceholder.typicode.com/404",
    {
      async onResponseError({ response }) { // onResponseError is a "ohmyfetch" method
        errorStatus = response.status; // assign the error to the declared variable "errorStatus"
      },
    }
  ); 

return { myData, error, errorStatus }; // returning all the variables

</script>

This is my first response to a GitHub issue, hope i wrote it at least understandable

If you feel you need the full error details on client side hydration, perhaps you could share your use case?

For FetchError, I want to get the http status code.

I tweaked this solution a little so it can be reusable:

// ~/composables/useAsyncDataWithError.ts
import type {
  AsyncData,
  KeyOfRes,
  PickFrom,
  _Transform,
} from 'nuxt/dist/app/composables/asyncData'
import type { AsyncDataOptions, NuxtApp } from '#app'

export default async function useAsyncDataWithError<
  DataT,
  DataE = Error,
  Transform extends _Transform<DataT> = _Transform<DataT, DataT>,
  PickKeys extends KeyOfRes<Transform> = KeyOfRes<Transform>,
>(
  key: string,
  handler: (ctx?: NuxtApp) => Promise<DataT>,
  options: AsyncDataOptions<DataT, Transform, PickKeys> = {},
): Promise<AsyncData<PickFrom<ReturnType<Transform>, PickKeys>, DataE | null | true>> {
  const serverError = useState<DataE | true | null>(`error-${key}`, () => null)
  const { error, data, ...rest } = await useAsyncData(key, handler, options)

  // Only set the value on server and if serverError is empty
  if (process.server && error.value && !serverError.value)
    serverError.value = error.value as DataE | true | null

  // Clear error if data is available
  if (data.value)
    serverError.value = null

  return {
    ...rest,
    data,
    error: serverError,
  }
}

Then somewhere in your app

const { error } = useAsyncDataWithError('key', () => $fetch('https://jsonplaceholder.typicode.com/404'))
// error value is same on both server and client

@pi0 I’m not using the error content for rendering, I want to use its content to make some checks on rendering. I want to detect the response status code whenever it’s 400, 404, 401, 403, 5xx… We won’t retry on 4xx errors without alteration to the request parameters, won’t we?

@pi0 @danielroe In useAsyncData we have transform option to transform the result data, how about if we add another option called transformError to alter the error? The default value of the transformError option can be a function that returns a simple data likes (error) => error.toString() or () => "An generic error message here..." to reduce the payload size. Then in my case I will able to get my favorite status code by providing my custom transformError:

<template>
  <div v-if="data">{{ data }}</div>
  <div v-else-if="error === 404">
     Page not found 
  </div>
  <div v-else>
     Error fetching data <button @click="refresh">retry</button>
  </div>
</template>
<script setup>
const { pending, error, data, refresh } = await useFetch(
  'https://jsonplaceholder.typicode.com/404',
  {
    transformError: err => err.response.status
  }
);
</script>

Another solution would be to change your backend server to return status 200 instead of 4xx for usage errors, that way you can read the error message/code from the payload and nothing private from the Nuxt server would be exposed.

No. Status codes are there for a reason. A serverside application returns a 404 for a reason: I cannot find what you are looking for. Give me the bloody error. I have no information with a boolean. I do not know if there is an internal server error, if a request is malformed, etc. Nuxt should not every decide for me that I am an absolute beginner thus that they need to hide useful error data.

Well-written rest API returns various error codes

And might include data in that response body. 404 is pretty self explanatory, but a 400 might include a message as to why your request is malformed.

@pi0 @danielroe In useAsyncData we have transform option to transform the result data, how about if we add another option called transformError to alter the error? The default value of the transformError option can be a function that returns a simple data likes (error) => error.toString() or () => "An generic error message here..." to reduce the payload size. Then in my case I will able to get my favorite status code by providing my custom transformError:

<template>
  <div v-if="data">{{ data }}</div>
  <div v-else-if="error === 404">
     Page not found 
  </div>
  <div v-else>
     Error fetching data <button @click="refresh">retry</button>
  </div>
</template>
<script setup>
const { pending, error, data, refresh } = await useFetch(
  'https://jsonplaceholder.typicode.com/404',
  {
    transformError: err => err.response.status
  }
);
</script>

This would be a perfect way of solving the problem imo.

I’m experiencing the same issue as @nndnha, in that I want the status code on the client side. For me it’s to show appropriate error UI, so that my users know exactly what’s gone wrong.

My way of solving this until now was for some useFetchs (well, technically useLazyAsyncDatas, but who’s counting) to only run on the client side, to ensure I have the full error context. In all cases, I was only ever looking at the status code anyway, and had a computed property to extract that from the rest of the context.

I think my ideal situation would be for the error property to always just be the error code, but I can see that there are cases where the rest of the error context could be useful, so being able to write a custom transformation strikes the perfect balance - offering full flexibility and ease of use, without making any assumptions for you.

I will also say that I appreciate Nuxt defaulting to hiding the error context because of the potential security issue it could cause - this is definitely the right approach to take for a default action (even if it does slightly hinder developer UX), and gives me great greater confidence that I’m not exposing myself to another issue somewhere else because I forgot to overwrite another default. Security first, additional functionality second.

I think both are valid points here. While error reference should be accessible per env for logging purposes, It is not meant for being serialized to payload or using its content to render.

Standard usage would be: (note that error is used as a boolean – hasError)

<template>
  <div v-if="data">{{ data }}</div>
  <div v-else-if="error">
    Error fetching data <button @click="refresh">retry</button>
  </div>
  <div v-else>Loading...</div>
</template>
<script setup>
const { pending, error, data, refresh } = await useFetch(
  'https://jsonplaceholder.typicode.com/404'
);
</script>

BTW if you really want to expose message, you can explicitly leak it with another state:

<template>
  <div v-if="data">{{ data }}</div>
  <div v-else-if="error">Error fetching data: {{ fetchError }}</div>
  <div v-else>Loading...</div>
</template>
<script setup>
const { data, error } = await useFetch('https://jsonplaceholder.typicode.com/404' );
const fetchError = useState('error', () => error.value.toString());
</script>

@danielroe BTW we might do better error hydration on the client-side, using a new Error instead of changing the type to boolean or probably better always make it boolean as hasError and provide a callback for onError handling by user this can also give a chance of implementing retry/fallback strategies easier.

It would be very helpful for me to have access to the error details. A request can fail for several reasons. Maybe the user is trying to access something that they haven’t been authorized to access, in which case we might inform the user of this (and possibly suggest that they request access from the owner). Or maybe the user made a typo and they’re (accidentally) trying to access something that doesn’t exist, in which case we might tell the user to check for typos. Or maybe the server is down, in which case we might provide the user with a “Try Again” button.

There is already some way to easily get the body when response code is other than 200?

Well-written rest API returns various error codes

In my case, I used promises.

const { pending, error, data } = await useFetch(url, {
  onResponse({ response }) {
    return new Promise((resolve, reject) => {
      response.ok ? resolve() : reject({ code: response.status, data: response.data })
    })
  }
})

if (error) {
  // error is the value of the reject argument.
}

Fair point about retrying on specific errors. Thinking how we can improve _errors then (into more general _state) that can sync state between client and server + next step after hydration (retry or error)

@danielroe Thanks for that solution, but @pi0 does care about the performance right? An extra duplicate request is a performance penalty for both server(internal server instead of jsonplaceholder.typicode.com) and client.