Skip to content
← Todas las preguntas
Intermedio

¿Cómo se gestionan los errores en composables asíncronos?

ComposablesManejo de errores

Devuelve una ref error junto a data e isLoading. El composable captura los errores internamente y los expone como estado reactivo, para que el componente pueda renderizar la UI de error sin bloques try/catch en el template. Nunca dejes que los errores escapen en silencio, y nunca lances excepciones desde un composable a menos que quien lo llame lo espere explícitamente.

Patrón básico

ts
// composables/useFetchData.ts
export function useFetchData<T>(url: string) {
  const data = ref<T | null>(null)
  const error = ref<Error | null>(null)
  const isLoading = ref(false)

  async function execute() {
    isLoading.value = true
    error.value = null

    try {
      data.value = await $fetch<T>(url)
    } catch (e) {
      error.value = e instanceof Error ? e : new Error(String(e))
      data.value = null
    } finally {
      isLoading.value = false
    }
  }

  execute()

  return { data, error, isLoading, retry: execute }
}
vue
<script setup>
const { data: users, error, isLoading, retry } = useFetchData<User[]>('/api/users')
</script>

<template>
  <div v-if="isLoading">Loading...</div>
  <div v-else-if="error">
    <p>Failed to load: {{ error.message }}</p>
    <button @click="retry">Try again</button>
  </div>
  <ul v-else-if="users">
    <li v-for="user in users" :key="user.id">{{ user.name }}</li>
  </ul>
</template>

El componente gestiona los tres estados (carga, error, éxito) de forma declarativa. La función retry permite al usuario recuperarse de fallos transitorios.

Por qué no lanzar excepciones

Si el composable lanza una excepción, el error se propaga hacia arriba y hace fallar el setup del componente. No hay nada que lo capture a menos que el componente envuelva la llamada en try/catch, lo que anula el propósito de que el composable abstraiga la lógica asíncrona:

ts
// MAL: lanzar desde un composable
export function useFetchData<T>(url: string) {
  const data = ref<T | null>(null)

  onMounted(async () => {
    data.value = await $fetch<T>(url) // lanza en error — hace fallar el componente
  })

  return { data }
}

Devolver una ref error le da al consumidor control total sobre cómo mostrar el error.

Observar URLs reactivas

Cuando la URL depende de estado reactivo, vuelve a cargar en cada cambio y gestiona los errores para cada petición:

ts
export function useFetchData<T>(url: MaybeRefOrGetter<string>) {
  const data = ref<T | null>(null)
  const error = ref<Error | null>(null)
  const isLoading = ref(false)

  async function execute() {
    const resolvedUrl = toValue(url)
    isLoading.value = true
    error.value = null

    try {
      data.value = await $fetch<T>(resolvedUrl)
    } catch (e) {
      error.value = e instanceof Error ? e : new Error(String(e))
      data.value = null
    } finally {
      isLoading.value = false
    }
  }

  watch(() => toValue(url), execute, { immediate: true })

  return { data, error, isLoading, retry: execute }
}
vue
<script setup>
const userId = ref(1)
const { data: user, error } = useFetchData<User>(
  () => `/api/users/${userId.value}`
)
</script>

Cada vez que userId cambia, el composable carga la nueva URL y resetea el estado de error.

Errores tipados para distintos tipos de fallo

Diferencia entre errores de red, errores de validación y errores de lógica de negocio:

ts
interface FetchResult<T> {
  data: Ref<T | null>
  error: Ref<FetchError | null>
  isLoading: Ref<boolean>
  retry: () => Promise<void>
}

interface FetchError {
  message: string
  status?: number
  isNetworkError: boolean
  isValidationError: boolean
}

function toFetchError(e: unknown): FetchError {
  if (e instanceof Response || (e && typeof e === 'object' && 'status' in e)) {
    const status = (e as any).status
    return {
      message: `Request failed with status ${status}`,
      status,
      isNetworkError: false,
      isValidationError: status === 422
    }
  }

  return {
    message: e instanceof Error ? e.message : String(e),
    isNetworkError: true,
    isValidationError: false
  }
}
vue
<template>
  <div v-if="error?.isNetworkError">
    Check your connection.
    <button @click="retry">Retry</button>
  </div>
  <div v-else-if="error?.isValidationError">
    The submitted data was invalid.
  </div>
  <div v-else-if="error">
    Something went wrong: {{ error.message }}
  </div>
</template>

Gestión global de errores con onErrorCaptured

Para errores que los composables no pueden gestionar (errores de ejecución inesperados), usa onErrorCaptured en un componente padre:

vue
<!-- ErrorBoundary.vue -->
<script setup>
const error = ref<Error | null>(null)

onErrorCaptured((err) => {
  error.value = err
  return false
})
</script>

<template>
  <div v-if="error">
    <p>Something went wrong: {{ error.message }}</p>
    <button @click="error = null">Dismiss</button>
  </div>
  <slot v-else />
</template>
vue
<!-- Uso -->
<ErrorBoundary>
  <UserProfile :user-id="1" />
</ErrorBoundary>

Esto captura cualquier error lanzado durante el renderizado o los lifecycle hooks de componentes hijos, evitando que toda la aplicación falle.

Lista de comprobación

PrácticaPor qué
Devuelve ref error, no lances excepcionesEl consumidor controla el renderizado del error
Resetea el error antes de cada peticiónLos errores obsoletos no persisten entre reintentos
Expone una función retryPermite a los usuarios recuperarse de fallos transitorios
Tipifica los errores por categoríaDiferentes errores necesitan diferente UI
Usa onErrorCaptured para errores inesperadosEvita fallos totales de la aplicación

Publicado bajo la licencia MIT.