8.0k New

Stack Trace

Displays a failed tool call and its stack frames.

Epicenter

Local-first, open source apps

Special Sponsor
Reset the database and seed it.

TypeError: Cannot read properties of undefined (reading 'email')

seedUsers in prisma/seed.ts:27 main in prisma/seed.ts:51

The migrations ran, but the seed failed in seedUsers. One fixture has no profile, so profile.email is undefined.

<script lang="ts">
  import FileCodeIcon from "@lucide/svelte/icons/file-code";
  import LayersIcon from "@lucide/svelte/icons/layers";
  import TerminalIcon from "@lucide/svelte/icons/terminal";
  import { Chat } from "@ai-sdk/svelte";
  import * as Bubble from "$lib/components/ui/bubble/index.js";
  import * as CodeBlock from "$lib/components/ui/code-block/index.js";
  import * as Message from "$lib/components/ui/message/index.js";
  import * as Trace from "$lib/components/ui/trace/index.js";
  import { createChat, getMessageText } from "$lib/ai-sdk.js";
  import { Button } from "$lib/components/ui/button/index.js";
  import type { UIDataTypes, UIMessage } from "ai";
 
  type ChatMessage = UIMessage<
    unknown,
    UIDataTypes,
    {
      runCommand: {
        input: { command: string };
        output: { stdout: string };
      };
    }
  >;
 
  type ToolPart = Extract<
    ChatMessage["parts"][number],
    { type: "tool-runCommand" }
  >;
 
  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 script = createChat<ChatMessage>()
    .user("Reset the database and seed it.")
    .assistant(({ writer }) => {
      writer
        .tool("runCommand", {
          input: { command: "pnpm prisma migrate reset --skip-seed" }
        })
        .sleep(900)
        .output({
          stdout: `Database reset successful
 
The following migrations have been applied:
 
migrations/
  └─ 20260912_init/
  └─ 20260920_add_profiles/`
        });
      writer
        .tool("runCommand", { input: { command: "pnpm prisma db seed" } })
        .sleep(1200)
        .error(STACK);
      writer.text(
        "The migrations ran, but the seed failed in seedUsers. One fixture has no profile, so profile.email is undefined."
      );
    });
 
  const transcript = script.get();
  const [question] = script.get(1);
  const transport = script.transport({ delayMs: 20 });
 
  const chat = new Chat<ChatMessage>({ messages: transcript, transport });
  const busy = $derived(
    chat.status === "submitted" || chat.status === "streaming"
  );
 
  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)
    };
  }
</script>
 
{#snippet stackTrace(stack: string)}
  {@const { error, appFrames, internalFrames } = parseStack(stack)}
  <Trace.Collapsible open>
    <Trace.CollapsibleTrigger>
      <Trace.Line status="error">
        <Trace.Icon>
          <TerminalIcon />
        </Trace.Icon>
        <Trace.Title>Failed: {error}</Trace.Title>
      </Trace.Line>
    </Trace.CollapsibleTrigger>
    <Trace.CollapsibleContent class="flex flex-col gap-2">
      <p class="font-mono text-xs text-destructive">{error}</p>
      <Trace.Root>
        {#each appFrames as frame, index (index)}
          <a href="#{frame.file}:{frame.lineNumber}">
            <Trace.Line>
              <Trace.Icon>
                <FileCodeIcon />
              </Trace.Icon>
              <Trace.Title>
                {frame.name} in {frame.file}:{frame.lineNumber}
              </Trace.Title>
            </Trace.Line>
          </a>
        {/each}
        <Trace.Collapsible>
          <Trace.CollapsibleTrigger>
            <Trace.Line>
              <Trace.Icon>
                <LayersIcon />
              </Trace.Icon>
              <Trace.Title>
                {internalFrames.length} framework frames
              </Trace.Title>
            </Trace.Line>
          </Trace.CollapsibleTrigger>
          <Trace.CollapsibleContent>
            <ol
              class="flex flex-col gap-1 overflow-x-auto rounded-md bg-muted p-3 font-mono text-xs"
            >
              {#each internalFrames as frame, index (index)}
                <li class="whitespace-nowrap">
                  {frame.line}
                </li>
              {/each}
            </ol>
          </Trace.CollapsibleContent>
        </Trace.Collapsible>
      </Trace.Root>
    </Trace.CollapsibleContent>
  </Trace.Collapsible>
{/snippet}
 
{#snippet commandRow(part: ToolPart, streaming: boolean)}
  {@const command = part.input?.command}
  {#if part.state === "output-error"}
    {@render stackTrace(part.errorText)}
  {:else if part.state === "output-available"}
    <Trace.Collapsible>
      <Trace.CollapsibleTrigger>
        <Trace.Line>
          <Trace.Icon>
            <TerminalIcon />
          </Trace.Icon>
          <Trace.Title>Ran {command}</Trace.Title>
        </Trace.Line>
      </Trace.CollapsibleTrigger>
      <Trace.CollapsibleContent>
        <CodeBlock.Root size="sm">
          <CodeBlock.Content wrap class="max-h-64" code={part.output.stdout} />
        </CodeBlock.Root>
      </Trace.CollapsibleContent>
    </Trace.Collapsible>
  {:else}
    <Trace.Line status={streaming ? "running" : "error"}>
      <Trace.Icon>
        <TerminalIcon />
      </Trace.Icon>
      <Trace.Title>
        {streaming ? "Running" : "Failed to run"}
        {command}
      </Trace.Title>
    </Trace.Line>
  {/if}
{/snippet}
 
<div class="flex min-h-72 w-full max-w-sm flex-col gap-4 py-6 text-sm">
  {#each chat.messages as message (message.id)}
    {#if message.role === "user"}
      <Message.Root align="end">
        <Message.Content>
          <Bubble.Root align="end" variant="muted">
            <Bubble.Content>{getMessageText(message)}</Bubble.Content>
          </Bubble.Root>
        </Message.Content>
      </Message.Root>
    {:else}
      {@const streaming = busy && message === chat.messages.at(-1)}
      {@const tools = message.parts.filter(
        (part) => part.type === "tool-runCommand"
      )}
      {@const text = getMessageText(message)}
      <Message.Root>
        <Message.Content class="gap-4">
          {#if tools.length > 0}
            <Trace.Root>
              {#each tools as part (part.toolCallId)}
                {@render commandRow(part, streaming)}
              {/each}
            </Trace.Root>
          {/if}
          {#if text}
            <p>{text}</p>
          {/if}
        </Message.Content>
      </Message.Root>
    {/if}
  {/each}
  {#if !busy}
    <Button
      variant="outline"
      size="sm"
      class="mt-auto self-start"
      onclick={() => {
        chat.messages = [];
        void chat.sendMessage(question);
      }}
    >
      Replay
    </Button>
  {/if}
</div>

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 Trace.Collapsible for the frames.

Installation

Stack Trace is built with the Trace and Code Block components.

pnpm dlx shadcn-svelte@latest add trace code-block

Usage

<script lang="ts">
  import * as Trace from "$lib/components/ui/trace/index.js";
 
  const [error, ...frames] = stack.split("\n").map((line) => line.trim());
</script>
<Trace.Root>
  <Trace.Collapsible>
    <Trace.CollapsibleTrigger>
      <Trace.Line status="error">
        <Trace.Icon>
          <CircleXIcon />
        </Trace.Icon>
        <Trace.Title>Failed: {error}</Trace.Title>
      </Trace.Line>
    </Trace.CollapsibleTrigger>
    <Trace.CollapsibleContent>
      <div
        class="flex flex-col gap-2 rounded-md bg-muted p-3 font-mono text-xs"
      >
        <p class="text-destructive">{error}</p>
        <ol>
          {#each frames as frame, index (index)}
            <li
              class={frame.includes("node_modules")
                ? undefined
                : "text-foreground"}
            >
              {frame}
            </li>
          {/each}
        </ol>
      </div>
    </Trace.CollapsibleContent>
  </Trace.Collapsible>
</Trace.Root>

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.

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')

formatInvoice in app/lib/billing.ts:42 InvoiceRow in app/components/invoice-row.tsx:18
<script lang="ts">
  import CircleXIcon from "@lucide/svelte/icons/circle-x";
  import FileCodeIcon from "@lucide/svelte/icons/file-code";
  import LayersIcon from "@lucide/svelte/icons/layers";
  import * as Trace from "$lib/components/ui/trace/index.js";
 
  const STACK = `TypeError: Cannot read properties of undefined (reading 'total')
    at formatInvoice (app/lib/billing.ts:42:31)
    at InvoiceRow (app/components/invoice-row.tsx:18:12)
    at renderWithHooks (node_modules/react-dom/cjs/react-dom.development.js:14985:18)
    at beginWork (node_modules/react-dom/cjs/react-dom.development.js:19049:16)
    at performUnitOfWork (node_modules/react-dom/cjs/react-dom.development.js:22776:12)
    at workLoopSync (node_modules/react-dom/cjs/react-dom.development.js:22707:5)`;
 
  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>
 
<Trace.Root class="max-w-sm">
  <Trace.Collapsible open>
    <Trace.CollapsibleTrigger>
      <Trace.Line status="error">
        <Trace.Icon>
          <CircleXIcon />
        </Trace.Icon>
        <Trace.Title>Failed: {error}</Trace.Title>
      </Trace.Line>
    </Trace.CollapsibleTrigger>
    <Trace.CollapsibleContent class="flex flex-col gap-2">
      <p class="font-mono text-xs text-destructive">{error}</p>
      <Trace.Root>
        {#each appFrames as frame, index (index)}
          <a href="#{frame.file}:{frame.lineNumber}">
            <Trace.Line>
              <Trace.Icon>
                <FileCodeIcon />
              </Trace.Icon>
              <Trace.Title>
                {frame.name} in {frame.file}:{frame.lineNumber}
              </Trace.Title>
            </Trace.Line>
          </a>
        {/each}
        <Trace.Collapsible>
          <Trace.CollapsibleTrigger>
            <Trace.Line>
              <Trace.Icon>
                <LayersIcon />
              </Trace.Icon>
              <Trace.Title>
                {internalFrames.length} framework frames
              </Trace.Title>
            </Trace.Line>
          </Trace.CollapsibleTrigger>
          <Trace.CollapsibleContent>
            <ol
              class="flex flex-col gap-1 overflow-x-auto rounded-md bg-muted p-3 font-mono text-xs"
            >
              {#each internalFrames as frame, index (index)}
                <li class="whitespace-nowrap">
                  {frame.line}
                </li>
              {/each}
            </ol>
          </Trace.CollapsibleContent>
        </Trace.Collapsible>
      </Trace.Root>
    </Trace.CollapsibleContent>
  </Trace.Collapsible>
</Trace.Root>

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.

src/routes/api/chat/+server.ts
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.

{#if part.state === "output-error"}
  {@const [error, ...frames] = part.errorText
    .split("\n")
    .map((line) => line.trim())}
  <Trace.Collapsible>…</Trace.Collapsible>
{/if}

Map the other tool states as in Tool.

API Reference

See Trace.