moeru-ai/airi · warning

No toast root provided, cannot close toast

Error message

No toast root provided, cannot close toast

What it means

toaster-pwa-update-ready.vue injects ToasterRootInjectionKey with a default object whose close() only logs this warning. The real close() is provided by toaster-root.vue; if the toast renders outside that provider tree, both the Update and Not-now buttons no-op with this warning and the toast can never be dismissed by id.

Source

Thrown at packages/stage-ui/src/components/scenarios/toasters/toaster-pwa-update-ready.vue:14

<script setup lang="ts">
import { Button } from '@proj-airi/ui'
import { inject } from 'vue'
import { useI18n } from 'vue-i18n'

import { ToasterRootInjectionKey } from './constants'

const props = defineProps<{ id?: string }>()

const emits = defineEmits<{ (e: 'update'): void }>()

const { t } = useI18n()

const toastRoot = inject(ToasterRootInjectionKey, { close: (id: string) => console.warn('No toast root provided, cannot close toast', id) })

function handleUpdate() {
  emits('update')
  toastRoot.close(props.id || '')
}

function handleNotNow() {
  toastRoot.close(props.id || '')
}
</script>

<template>
  <div
    :class="[
      'px-4 py-3',
      'backdrop-blur-md shadow-md',
      'bg-neutral-100/80 dark:bg-neutral-800/80',
      'w-full flex flex-col gap-1 rounded-2xl',

View on GitHub (pinned to 677329427f)

Solutions

  1. Render PWA update toasts inside the component tree that includes ToasterRoot, which provides the real close via ToasterRootInjectionKey
  2. In stories/tests, provide a matching { close } implementation at the test root or assert against this warning
  3. Prefer injecting without a default and disabling the buttons when the injection is absent, so misuse is explicit

Example fix

// before
const toastRoot = inject(ToasterRootInjectionKey, { close: (id: string) => console.warn('No toast root provided, cannot close toast', id) })

// after
const toastRoot = inject(ToasterRootInjectionKey, undefined)
function closeToast() {
  if (!toastRoot || !props.id)
    return
  toastRoot.close(props.id)
}
Defensive patterns

Strategy: type-guard

Validate before calling

// At the app root, always wrap toasts in the provider
// <ToasterRoot> ... <ToasterPwaUpdateReady /> ... </ToasterRoot>

Type guard

const toastRoot = inject(ToasterRootInjectionKey, undefined)
function canCloseToast(id?: string): id is string {
  return Boolean(toastRoot) && typeof id === 'string' && id.length > 0
}

Prevention

When it happens

Trigger: Mounting toaster-pwa-update-ready.vue without an ancestor <ToasterRoot> (the component calling provide(ToasterRootInjectionKey, { close })): using the toast in isolation, in stories/tests, or teleported/mounted to a root that is outside the toaster provider subtree.

Common situations: Storybook/Histoire usage without the provider; refactoring the toaster layout so the root wrapper is lost; mounting the toast on a separate Vue app instance.

Related errors


AI-assisted analysis of moeru-ai/airi@677329427f (2026-08-18). Data as JSON: /api/errors/ffccfae01ab171be. Report an issue: GitHub.