The output panel

Filter results, logs and errors, explore values as trees or tables, follow stack frames and copy what a run produced.

Every result, log and error appears as a row in the output panel, tagged with the line that produced it.

Lines and rows

Hover an output row to light its line, move the caret to light its rows, and click a row's line number to jump there.

// Move the caret between these lines and watch the output follow.
const width = 12;
const height = 5;
width * height;
width + height;
Math.hypot(width, height);

Every row knows its line. Hovering a row highlights that line in the editor, and putting the caret on a line highlights its rows in the output. Click a row’s line number to move the caret there.

Filters

Results, logs and errors each get a row tagged with their line, with filters, Copy All and Clear.

const sum = 2 + 2;
sum;
console.log("a log row");
console.info("docs: https://js-lab.app/docs/output");
process.stdout.write("raw stdout\n");
["results", "logs", "errors"].length;

Four filter chips sit above the output: All, Results, Logs and Errors. Each shows a live count, and the counts stay visible at zero. Pick a chip to narrow what’s shown. stderr and a failed console.assert count as errors; console.info and console.warn count as logs.

The same filters are commands: Output: Show All, Output: Show Results, Output: Show Logs and Output: Show Errors, in the command palette.

Text written with process.stdout.write and process.stderr.write appears as its own rows, without a line number. When the panel is empty, the first rows of a run fade in (not with Reduce Motion).

The console API

Groups, counters, timers, tables and the rest of console work, each row tagged with its line.

console.group("Checkout");
console.count("step");
console.count("step");
console.time("pricing");
const total = [9.99, 19.5].reduce((a, b) => a + b, 0);
console.timeEnd("pricing");
console.table([{ item: "pen", price: 9.99 }, { item: "book", price: 19.5 }]);
console.groupEnd();
total;

All of console works in every runtime: log, info, warn, error, debug, dir, dirxml, table, trace (with its stack), assert, count and countReset, time, timeLog and timeEnd, and group, groupCollapsed and groupEnd, which indent the rows between them. console.clear() clears the panel. Each row is tagged with the line that called it.

Errors and stack frames

An error names the line it came from, underlines it in the editor, and links each stack frame to your code.

function parsePrice(text: string) {
  const value = Number.parseFloat(text);
  if (Number.isNaN(value)) {
    throw new Error(`Not a price: "${text}"`, { cause: { input: text } });
  }
  return value;
}

parsePrice("9.99");
parsePrice("free");

An error row is tinted and marked in the editor margin. Its stack frames are mapped back to your source lines, so they point at the code you wrote. Click a frame to jump to it. Frames from outside your code aren’t listed; a note says how many internal frames were left out. An error’s cause is shown with it, and a syntax error shows a code frame.

An uncaught exception or an unhandled promise rejection (“Uncaught (in promise)”) becomes an error row too. While you type on a line, its error underline waits until you leave the line or pause for a second.

Promises and async

A logged promise shows as pending, then updates in place when it settles.

const later = (value: string, ms: number) => new Promise<string>((resolve) => setTimeout(() => resolve(value), ms));

later("ready after a second", 1000);
later("ready after two", 2000);
await later("awaited", 500);

A logged promise shows as Promise { <pending> }, and the same row updates when it settles. Top-level await works anywhere in a tab.

Expanding values

Objects, arrays, Maps and Sets expand a level at a time, and big ones load more as you ask.

const report = {
  get generatedAt() {
    return new Date().toISOString();
  },
  totals: { orders: 128, refunds: 3, nested: { level: { deeper: { deepest: "found" } } } },
  ids: Array.from({ length: 1500 }, (_, i) => i),
};
report;
  • Objects, arrays, Maps and Sets expand on click, one level at a time.
  • ⌥→, or Option-click on the disclosure arrow, expands everything under a value at once.
  • A large Array, Map, Set or typed array offers … N more entries to load the next page. A plain object isn’t paged: past the property limit, it states how many properties remain.
  • Values arrive three levels deep; deeper parts load when you open them. A getter runs only when you click it.
  • http:// and https:// links in results, console arguments, stdout, stderr and error messages are clickable. ⌘-click one, or Tab to it and press Return or Space, to open it in your browser. Links with other schemes, and URLs that carry credentials, are never made clickable.

Kinds of values

Dates, RegExps, URLs, BigInts, Symbols, typed arrays, errors and more each show in their own form.

[
  new Date(),
  /colou?r/gi,
  new URL("https://js-lab.app/docs?tab=1"),
  2n ** 64n,
  Symbol("token"),
  new Uint8Array([1, 2, 3]),
  new Map([["a", 1]]),
  new Set(["x", "y"]),
  new Error("outer", { cause: new Error("inner") }),
  class Point {},
];

Each kind of value has its own form: dates, regular expressions, URLs, symbols, BigInts, errors with their cause, promises with their state, functions and classes, typed arrays and ArrayBuffer bytes, Headers and Response, WeakMap, WeakSet and WeakRef, proxies, circular references, holes in sparse arrays, and objects with a null prototype. In a browser tab, a DOM element shows as its tag and expands to its attributes and HTML.

Changed values

After a re-run, rows whose values changed flash briefly, so you see what your edit affected.

// Change 2 to 3: the two rows below flash.
const base = 2;
base * 10;
base * 100;
"this row never changes";

After a re-run, a row whose value changed tints for a moment, so you can see what an edit affected. Rows on the line you’re typing on don’t flash.

Tables

Any list of records can switch to a table you can sort, filter and copy as CSV, JSON or Markdown.

const people = Array.from({ length: 60 }, (_, i) => ({
  id: i,
  name: `user${i}`,
  score: (i * 37) % 100,
  team: ["red", "green", "blue"][i % 3],
}));
people;
console.table(people.slice(0, 5));

A row whose value is a list of records — an array (or object) of objects, like an API response or a parsed CSV — offers a Tree / Table switch at its right edge. console.table() opens as a table straight away.

  • Click a column header to sort ascending, again for descending, a third time for the original order. Numbers sort as numbers; empty cells sort last.
  • Filter rows keeps the rows where any cell contains what you type.
  • Copy as CSV, JSON or Markdown copies the rows shown, filtered and sorted.
  • A cell holding an object or array expands in place.
  • The table shows its first 50 rows. Show all, or the ⤢ button, opens it full-size over the window, where Load more fetches the rest of a large collection. Esc closes it.
  • The choice of Tree or Table is remembered for that line while JSLab is open, so a re-run keeps the table.
  • In the command palette: Output: Toggle Table View and Output: Open Table act on the row on the caret’s line.

Find in Value

Search a large value's keys and values, and step through every match.

const response = {
  users: [
    { name: "Ada", city: "London" },
    { name: "Grace", city: "Arlington" },
    { name: "Linus", city: "Oslo" },
  ],
  meta: { page: 1, region: "eu-north" },
};
response;

Right-click a row whose value can be expanded and choose Find in Value… to search inside it. Keys and values both match; the way to each match opens, and ↩ and ⇧↩ step through them. It searches only what has loaded, and says so when it skipped values you haven’t opened. Esc closes it.

JSON inside strings

A string that holds JSON gets a { } toggle that shows it as a tree you can expand.

const payload = JSON.stringify({ status: "ok", items: [{ id: 1, price: 9.99 }, { id: 2, price: 19.5 }], next: null });
payload;

A string that holds a JSON object or array shows a { } toggle beside it. Turn it on to read the JSON as a tree, with every tree tool; turn it off for the original string.

Previews

Colours get a swatch, image data URLs a thumbnail, dates their age, and big values a size badge.

const svg = '<svg xmlns="http://www.w3.org/2000/svg" width="40" height="40"><rect width="40" height="40" rx="8" fill="#f8fafc"/><circle cx="20" cy="20" r="12" fill="#f59e0b"/></svg>';
({
  primary: "#3b82f6",
  accent: "oklch(70% 0.15 330)",
  logo: `data:image/svg+xml;base64,${btoa(svg)}`,
  updated: new Date(Date.now() - 2 * 3_600_000),
  notes: "lorem ipsum ".repeat(30),
});

Small previews sit beside values:

  • A colour such as #3b82f6, rgb(…) or oklch(…) shows a swatch; a word like "red" doesn’t.
  • A data:image/…;base64 string shows a thumbnail when you hover it. Images from web addresses are never loaded.
  • A date says how long ago it was, such as “· 2 hours ago”.
  • A collapsed object shows how many keys it has, and a string longer than 200 characters shows its length.

After a syntax error

A syntax error doesn’t clear the panel. The last successful output stays on screen, dimmed and labeled Last successful run.

Very large output

Output is bounded, so a huge string or a flood of entries can’t freeze the window. When a run produces more entries than the limit, a note says how many were dropped. Raise the limit with Output Limit in Settings → Advanced: 10,000 entries by default, from 100 to 100,000.

Copying

Right-click any part of a value to copy its path, its value, or that part as JSON.

const api = {
  users: [
    { id: 1, name: "Ada", "my-key": "first" },
    { id: 2, name: "Linus", "my-key": "second", tags: ["ops", "admin"] },
  ],
  lookup: new Map([["b", { n: 2 }]]),
};
api;

Right-click a row, open its menu button, or press ⇧F10 while it’s focused, for:

  • Copy: the row’s value as plain text.
  • Copy as JSON: the row’s value as JSON. It never fails and never yields a bare undefined: circular references become "[Circular]", NaN and ±Infinity keep their names, and Map entries become pairs. It copies exactly what’s on screen, so a truncated value copies as the truncated text.
  • Explain Result: asks the AI assistant about this row. See Ask AI.
  • Find in Value…: on a value that expands.

Right-click any part of an expanded value for Copy Path — a JavaScript path to that part from $, the logged value, such as $.users[3]["my-key"] — Copy Value and Copy as JSON of just that part. A Map entry’s path uses .get(key); where no path reaches, such as a Set’s items, Copy Path is unavailable.

Right-click empty space in the panel for Copy All, which copies every entry the current filter shows, and Clear (⌘K).

Display options

Settings → Appearance has two independent toggles for the panel: Output Highlighting colors values by type, and Output Line Numbers shows the source line beside each row. Both are on by default.

To hide the panel itself, use View → Output, or Toggle Output Panel in the command palette.

Last updated