- Accordion
- Alert
- Alert Dialog
- Aspect Ratio
- Attachment
- Avatar
- Badge
- Breadcrumb
- Bubble
- Button
- Button Group
- Calendar
- Card
- Carousel
- Chart
- Checkbox
- Code Block
- Collapsible
- Combobox
- Command
- Context Menu
- Data Table
- Date Picker
- Dialog
- Drawer
- Dropdown Menu
- Empty
- Field
- Form
- Hover Card
- Input
- Input Group
- Input OTP
- Item
- Kbd
- Label
- Marker
- Menubar
- Message
- Message Scroller
- Native Select
- Navigation Menu
- Number Field
- Pagination
- Pin Input
- Popover
- Progress
- Questionnaire
- Radio Group
- Range Calendar
- Resizable
- Scroll Area
- Select
- Separator
- Sheet
- Sidebar
- Skeleton
- Slider
- Sonner
- Spinner
- Stepper
- Switch
- Table
- Tabs
- Tags Input
- Textarea
- Toast
- Toggle
- Toggle Group
- Tooltip
- Trace
- Typography
TypeError: Cannot read properties of undefined (reading 'email')
The migrations ran, but the seed failed in seedUsers. One fixture has no profile, so profile.email is undefined.
<script setup lang="ts">
import { FileCodeIcon, LayersIcon, TerminalIcon } from '@lucide/vue'
import { promiseTimeout, tryOnScopeDispose } from '@vueuse/core'
import { ref } from 'vue'
import { Bubble, BubbleContent } from '@/components/ui/bubble'
import { Button } from '@/components/ui/button'
import { CodeBlock, CodeBlockContent } from '@/components/ui/code-block'
import { Message, MessageContent } from '@/components/ui/message'
import {
Trace,
TraceCollapsible,
TraceCollapsibleContent,
TraceCollapsibleTrigger,
TraceIcon,
TraceLine,
TraceTitle,
} from '@/components/ui/trace'
type ToolPart
= | { toolCallId: string, state: 'input-available', input: { command: string } }
| {
toolCallId: string
state: 'output-available'
input: { command: string }
output: { stdout: string }
}
| {
toolCallId: string
state: 'output-error'
input: { command: string }
errorText: string
error: ReturnType<typeof parseStack>
}
const QUESTION = 'Reset the database and seed it.'
const ANSWER
= 'The migrations ran, but the seed failed in seedUsers. One fixture has no profile, so profile.email is undefined.'
const STACK = `TypeError: Cannot read properties of undefined (reading 'email')
at seedUsers (prisma/seed.ts:27:34)
at main (prisma/seed.ts:51:9)
at Module._compile (node:internal/modules/cjs/loader:1554:14)
at Object.transformer (node_modules/tsx/dist/register.cjs:3:1104)
at Module.load (node:internal/modules/cjs/loader:1289:32)
at process.processTicksAndRejections (node:internal/process/task_queues:105:5)`
const RESET_OUTPUT = `Database reset successful
The following migrations have been applied:
migrations/
└─ 20260912_init/
└─ 20260920_add_profiles/`
function parseFrame(line: string) {
const match = line.match(/^at (.+) \((.+):(\d+):(\d+)\)$/)
return {
line,
name: match?.[1],
file: match?.[2],
lineNumber: match?.[3],
internal: line.includes('node_modules') || line.includes('node:'),
}
}
function parseStack(stack: string) {
const [error, ...lines] = stack.split('\n').map(line => line.trim())
const frames = lines.map(parseFrame)
return {
error,
appFrames: frames.filter(frame => !frame.internal),
internalFrames: frames.filter(frame => frame.internal),
}
}
function finalTools(): ToolPart[] {
return [
{
toolCallId: 'reset',
state: 'output-available',
input: { command: 'pnpm prisma migrate reset --skip-seed' },
output: { stdout: RESET_OUTPUT },
},
{
toolCallId: 'seed',
state: 'output-error',
input: { command: 'pnpm prisma db seed' },
errorText: STACK,
error: parseStack(STACK),
},
]
}
const tools = ref<ToolPart[]>(finalTools())
const text = ref(ANSWER)
const busy = ref(false)
// Unmounting bumps `run`, so a running script stops at its next wait.
let run = 0
tryOnScopeDispose(() => run++)
async function wait(ms: number, id: number) {
await promiseTimeout(ms)
return id === run
}
async function replay() {
const id = ++run
busy.value = true
text.value = ''
const [reset, seed] = finalTools()
tools.value = [{ toolCallId: reset!.toolCallId, state: 'input-available', input: reset!.input }]
if (!(await wait(900, id)))
return
tools.value = [reset!]
tools.value = [reset!, { toolCallId: seed!.toolCallId, state: 'input-available', input: seed!.input }]
if (!(await wait(1200, id)))
return
tools.value = [reset!, seed!]
for (const word of ANSWER.split(/(?<= )/)) {
if (!(await wait(20, id)))
return
text.value += word
}
busy.value = false
}
</script>
<template>
<div class="flex min-h-72 w-full max-w-sm flex-col gap-4 py-6 text-sm">
<Message align="end">
<MessageContent>
<Bubble align="end" variant="muted">
<BubbleContent>{{ QUESTION }}</BubbleContent>
</Bubble>
</MessageContent>
</Message>
<Message v-if="tools.length > 0 || text">
<MessageContent class="gap-4">
<Trace v-if="tools.length > 0">
<template v-for="part in tools" :key="part.toolCallId">
<TraceCollapsible v-if="part.state === 'output-error'" default-open>
<TraceCollapsibleTrigger>
<TraceLine status="error">
<TraceIcon>
<TerminalIcon />
</TraceIcon>
<TraceTitle>Failed: {{ part.error.error }}</TraceTitle>
</TraceLine>
</TraceCollapsibleTrigger>
<TraceCollapsibleContent class="flex flex-col gap-2">
<p class="font-mono text-xs text-destructive">
{{ part.error.error }}
</p>
<Trace>
<a
v-for="frame in part.error.appFrames"
:key="frame.line"
:href="`#${frame.file}:${frame.lineNumber}`"
>
<TraceLine>
<TraceIcon>
<FileCodeIcon />
</TraceIcon>
<TraceTitle>
{{ frame.name }} in {{ frame.file }}:{{ frame.lineNumber }}
</TraceTitle>
</TraceLine>
</a>
<TraceCollapsible>
<TraceCollapsibleTrigger>
<TraceLine>
<TraceIcon>
<LayersIcon />
</TraceIcon>
<TraceTitle>
{{ part.error.internalFrames.length }} framework frames
</TraceTitle>
</TraceLine>
</TraceCollapsibleTrigger>
<TraceCollapsibleContent>
<ol class="flex flex-col gap-1 overflow-x-auto rounded-md bg-muted p-3 font-mono text-xs">
<li
v-for="frame in part.error.internalFrames"
:key="frame.line"
class="whitespace-nowrap"
>
{{ frame.line }}
</li>
</ol>
</TraceCollapsibleContent>
</TraceCollapsible>
</Trace>
</TraceCollapsibleContent>
</TraceCollapsible>
<TraceCollapsible v-else-if="part.state === 'output-available'">
<TraceCollapsibleTrigger>
<TraceLine>
<TraceIcon>
<TerminalIcon />
</TraceIcon>
<TraceTitle>Ran {{ part.input.command }}</TraceTitle>
</TraceLine>
</TraceCollapsibleTrigger>
<TraceCollapsibleContent>
<CodeBlock size="sm">
<CodeBlockContent wrap class="max-h-64">
{{ part.output.stdout }}
</CodeBlockContent>
</CodeBlock>
</TraceCollapsibleContent>
</TraceCollapsible>
<TraceLine v-else :status="busy ? 'running' : 'error'">
<TraceIcon>
<TerminalIcon />
</TraceIcon>
<TraceTitle>
{{ busy ? 'Running' : 'Failed to run' }} {{ part.input.command }}
</TraceTitle>
</TraceLine>
</template>
</Trace>
<p v-if="text">
{{ text }}
</p>
</MessageContent>
</Message>
<Button
v-if="!busy"
variant="outline"
size="sm"
class="mt-auto self-start"
@click="replay"
>
Replay
</Button>
</div>
</template>Stack Trace is a line for a tool call that failed. The error is the title, like "Failed: TypeError". Open it for the stack frames, which can link to the file and line.
Stack Trace is built with Trace with a TraceCollapsible for the frames.
Installation
Stack Trace is built with the Trace and Code Block components.
pnpm dlx shadcn-vue@latest add trace code-block
Usage
<script setup lang="ts">
import { CircleXIcon } from '@lucide/vue'
import { computed } from 'vue'
import {
Trace,
TraceCollapsible,
TraceCollapsibleContent,
TraceCollapsibleTrigger,
TraceIcon,
TraceLine,
TraceTitle,
} from '@/components/ui/trace'
const props = defineProps<{ stack: string }>()
const lines = computed(() => props.stack.split('\n').map(line => line.trim()))
const error = computed(() => lines.value[0])
const frames = computed(() => lines.value.slice(1))
</script>
<template>
<Trace>
<TraceCollapsible>
<TraceCollapsibleTrigger>
<TraceLine status="error">
<TraceIcon>
<CircleXIcon />
</TraceIcon>
<TraceTitle>Failed: {{ error }}</TraceTitle>
</TraceLine>
</TraceCollapsibleTrigger>
<TraceCollapsibleContent>
<div class="flex flex-col gap-2 rounded-md bg-muted p-3 font-mono text-xs">
<p class="text-destructive">
{{ error }}
</p>
<ol>
<li
v-for="(frame, index) in frames"
:key="index"
:class="frame.includes('node_modules') ? undefined : 'text-foreground'"
>
{{ frame }}
</li>
</ol>
</div>
</TraceCollapsibleContent>
</TraceCollapsible>
</Trace>
</template>The title truncates, so repeat the full error at the top of the content. Frames from node_modules stay muted, so your own frames stand out.
Frame Links
Turn your own frames into links that open the file at the line. Fold the framework frames into a collapsible line of their own, so the frames you can fix come first.
TypeError: Cannot read properties of undefined (reading 'total')
<script setup lang="ts">
import { CircleXIcon, FileCodeIcon, LayersIcon } from '@lucide/vue'
import {
Trace,
TraceCollapsible,
TraceCollapsibleContent,
TraceCollapsibleTrigger,
TraceIcon,
TraceLine,
TraceTitle,
} from '@/components/ui/trace'
const STACK = `TypeError: Cannot read properties of undefined (reading 'total')
at formatInvoice (app/utils/billing.ts:42:31)
at setup (app/components/InvoiceRow.vue:18:12)
at callWithErrorHandling (node_modules/@vue/runtime-core/dist/runtime-core.cjs.js:200:19)
at setupStatefulComponent (node_modules/@vue/runtime-core/dist/runtime-core.cjs.js:7848:25)
at setupComponent (node_modules/@vue/runtime-core/dist/runtime-core.cjs.js:7809:36)
at mountComponent (node_modules/@vue/runtime-core/dist/runtime-core.cjs.js:5159:7)`
function parseFrame(line: string) {
const match = line.match(/^at (.+) \((.+):(\d+):(\d+)\)$/)
return {
line,
name: match?.[1],
file: match?.[2],
lineNumber: match?.[3],
internal: line.includes('node_modules'),
}
}
const [error, ...lines] = STACK.split('\n').map(line => line.trim())
const frames = lines.map(parseFrame)
const appFrames = frames.filter(frame => !frame.internal)
const internalFrames = frames.filter(frame => frame.internal)
</script>
<template>
<Trace class="max-w-sm">
<TraceCollapsible default-open :unmount-on-hide="false">
<TraceCollapsibleTrigger>
<TraceLine status="error">
<TraceIcon>
<CircleXIcon />
</TraceIcon>
<TraceTitle>Failed: {{ error }}</TraceTitle>
</TraceLine>
</TraceCollapsibleTrigger>
<TraceCollapsibleContent class="flex flex-col gap-2">
<p class="font-mono text-xs text-destructive">
{{ error }}
</p>
<Trace>
<a
v-for="frame in appFrames"
:key="frame.line"
:href="`#${frame.file}:${frame.lineNumber}`"
>
<TraceLine>
<TraceIcon>
<FileCodeIcon />
</TraceIcon>
<TraceTitle>
{{ frame.name }} in {{ frame.file }}:{{ frame.lineNumber }}
</TraceTitle>
</TraceLine>
</a>
<TraceCollapsible>
<TraceCollapsibleTrigger>
<TraceLine>
<TraceIcon>
<LayersIcon />
</TraceIcon>
<TraceTitle>
{{ internalFrames.length }} framework frames
</TraceTitle>
</TraceLine>
</TraceCollapsibleTrigger>
<TraceCollapsibleContent>
<ol class="flex flex-col gap-1 overflow-x-auto rounded-md bg-muted p-3 font-mono text-xs">
<li
v-for="frame in internalFrames"
:key="frame.line"
class="whitespace-nowrap"
>
{{ frame.line }}
</li>
</ol>
</TraceCollapsibleContent>
</TraceCollapsible>
</Trace>
</TraceCollapsibleContent>
</TraceCollapsible>
</Trace>
</template>AI SDK
When a tool throws, its part is output-error. By default errorText is only the error message, with no frames. Return error.stack from onError in toUIMessageStreamResponse to send them.
return result.toUIMessageStreamResponse({
onError: error =>
error instanceof Error ? (error.stack ?? error.message) : String(error),
})A stack shows file paths from your server, so only send it in development.
<script setup lang="ts">
const [error, ...frames] = part.errorText
.split('\n')
.map(line => line.trim())
</script>
<template>
<TraceCollapsible v-if="part.state === 'output-error'">
…
</TraceCollapsible>
</template>Map the other tool states as in Tool.
Migrating from AI Elements Vue
If you added Stack Trace from AI Elements Vue, your project has a components/ai-elements/stack-trace/ folder. Paste this prompt into your coding agent to rebuild it on Trace. Every export keeps its name and props, so the code that uses it keeps working.
Migrate components/ai-elements/stack-trace/ from AI Elements Vue to the shadcn-vue Trace component.
Keep every export, with the same name and props, so the code that imports it keeps working. Only change how each part renders.
1. Install Trace: npx shadcn-vue@latest add trace
2. Read https://www.shadcn-vue.com/docs/ai-elements/stack-trace and build Stack Trace the same way.
3. Rebuild each part:
- StackTrace renders a Trace with a TraceCollapsible. Keep trace, open, defaultOpen, the update:open and filePathClick events, and the parsing.
- StackTraceHeader renders a TraceCollapsibleTrigger with a TraceLine with status="error" and a TraceIcon.
- StackTraceError renders a TraceTitle, like "Failed: TypeError: …". Keep StackTraceErrorType and StackTraceErrorMessage as spans inside it.
- StackTraceContent renders a TraceCollapsibleContent.
- StackTraceFrames renders the frames as a list in a muted, monospace block. Dim the frames from node_modules, and keep the file path buttons.
- StackTraceExpandButton renders nothing, since the whole line opens the frames.
- Buttons cannot sit inside a trigger. Render StackTraceActions and StackTraceCopyButton next to the TraceCollapsibleTrigger, not inside it.
4. Remove the imports the files no longer use.
5. Run vue-tsc and fix any errors.API Reference
See Trace.
Sponsors
Your logo hereshadcn-vue is free and open source. Sponsorship keeps it maintained.
Become a sponsor