Parse V8 Stack Traces Into Frames

Turn V8 stack strings into structured frames with function names, file paths, line and column numbers, async and native markers.

TypescriptCli Tooling

Code

snippet
export interface StackFrame {
  readonly functionName: string | null;
  readonly file: string | null;
  readonly line: number | null;
  readonly column: number | null;
  readonly native: boolean;
  readonly async: boolean;
}

export function parseStackTrace(stack: string): StackFrame[] {
  if (typeof stack !== "string") throw new TypeError("stack must be a string");
  const frames: StackFrame[] = [];
  for (const rawLine of stack.split("\n")) {
    const line = rawLine.trim();
    if (!line.startsWith("at ")) continue;
    const frame = parseFrame(line.slice(3));
    if (frame !== null) frames.push(frame);
  }
  return frames;
}

function parseFrame(value: string): StackFrame | null {
  if (value === "native") {
    return { functionName: null, file: null, line: null, column: null, native: true, async: false };
  }
  let text = value;
  let isAsync = false;
  if (text.startsWith("async ")) {
    isAsync = true;
    text = text.slice(6);
  }
  const locationMatch = /^(.*?) \((.+):(\d+):(\d+)\)$/.exec(text);
  if (locationMatch !== null) {
    return {
      functionName: normalizeName(locationMatch[1] ?? ""),
      file: locationMatch[2] ?? null,
      line: Number(locationMatch[3]),
      column: Number(locationMatch[4]),
      native: false,
      async: isAsync,
    };
  }
  const bareMatch = /^(.+):(\d+):(\d+)$/.exec(text);
  if (bareMatch !== null) {
    return {
      functionName: null,
      file: bareMatch[1] ?? null,
      line: Number(bareMatch[2]),
      column: Number(bareMatch[3]),
      native: false,
      async: isAsync,
    };
  }
  return {
    functionName: normalizeName(text),
    file: null,
    line: null,
    column: null,
    native: false,
    async: isAsync,
  };
}

function normalizeName(value: string): string | null {
  const name = value.trim();
  if (name === "" || name === "eval") return null;
  return name;
}

const stack = [
  "Error: boom",
  "    at fetchUser (/app/src/users.js:42:13)",
  "    at async loadProfile (/app/src/profile.js:8:20)",
  "    at /app/src/index.js:3:1",
  "    at native",
].join("\n");
console.log(parseStackTrace(stack));
console.log(parseStackTrace("Error: empty"));