Vercel Logo

Wrap the Full Lifecycle in a Function

Every working script eventually wants to be a function. Ours is no exception.

Sandboxes stop automatically when their configured timeout expires; the default session timeout is five minutes. Prompt cleanup still matters because idle time consumes resources until that safeguard runs. Before we hand this off to a CLI, we'll wrap the lifecycle in try/finally so stop() runs as soon as the work finishes or throws.

Outcome

Refactor the script into an exported runSandboxLifecycle(repoUrl) function that wraps create → clone → read in try/finally, always calls stop(), and returns a structured result.

Fast Track

  1. Extract the body into export async function runSandboxLifecycle(repoUrl: string).
  2. Wrap the work in try/finally with sandbox.stop() in finally.
  3. Return { sandboxName, cloneExitCode, files, readmePreview }.

Hands-on exercise

Restructure src/sandbox-lifecycle.ts. We're keeping all the logic from the last lesson, just reorganizing it.

import { Sandbox } from '@vercel/sandbox';
 
export type LifecycleResult = {
  sandboxName: string;
  cloneExitCode: number;
  files: string;
  readmePreview: string;
};
 
export async function runSandboxLifecycle(repoUrl: string): Promise<LifecycleResult> {
  const sandbox = await Sandbox.create({ persistent: false, timeout: 10 * 60 * 1000 });
 
  try {
    const clone = await sandbox.runCommand('git', ['clone', '--depth', '1', repoUrl, 'repo']);
    if (clone.exitCode !== 0) {
      throw new Error(`Clone failed: ${await clone.stderr()}`);
    }
 
    const ls = await sandbox.runCommand('ls', ['-la', 'repo']);
 
    let readmePreview = '(no README found)';
    const readme = await sandbox.readFileToBuffer({ path: 'repo/README.md' });
    if (readme) {
      readmePreview = readme.toString('utf8').slice(0, 300);
    }
 
    return {
      sandboxName: sandbox.name,
      cloneExitCode: clone.exitCode,
      files: await ls.stdout(),
      readmePreview
    };
  } finally {
    await sandbox.stop();
  }
}

Two changes worth pointing out. First, a failed clone now throws instead of returning early. That lets the caller decide how to handle it, and finally still cleans up the Sandbox either way. Second, we removed the console.log calls. Logging is the CLI's job, not the lifecycle's.

To verify the function still works end-to-end, add a small test caller below the function (we'll delete this when the CLI takes over):

async function main() {
  const result = await runSandboxLifecycle('https://github.com/vercel/examples');
  console.log(result);
}
 
main();
Troubleshooting: forgot to await stop

If the script exits before sandbox.stop() finishes, you might leak Sandboxes. await is doing real work here, not just type comfort.

Troubleshooting: should clone failures throw?

Throwing makes failed clones look the same as unexpected crashes to the caller. If you'd rather distinguish them, return cloneExitCode and a success: boolean field instead. For this course we're keeping it simple.

Try It

pnpm tsx src/sandbox-lifecycle.ts

Expected output:

{
  sandboxName: 'repo-review-...',
  cloneExitCode: 0,
  files: 'total 32\ndrwxr-xr-x  ... README.md\n...',
  readmePreview: '# Vercel Examples\n\nThis repository contains...'
}

One object, ready to be consumed by something else. That something else is the CLI we build next.

Commit

git add src/sandbox-lifecycle.ts
git commit -m "feat(sandbox): wrap lifecycle in a reusable function with try/finally"

Done-When

  • runSandboxLifecycle(repoUrl) is exported and accepts a URL
  • Body is wrapped in try/finally
  • sandbox.stop() runs in finally even on throw
  • Returns { sandboxName, cloneExitCode, files, readmePreview }

Solution

src/sandbox-lifecycle.ts
import { Sandbox } from '@vercel/sandbox';
 
export type LifecycleResult = {
  sandboxName: string;
  cloneExitCode: number;
  files: string;
  readmePreview: string;
};
 
export async function runSandboxLifecycle(repoUrl: string): Promise<LifecycleResult> {
  const sandbox = await Sandbox.create({ persistent: false, timeout: 10 * 60 * 1000 });
 
  try {
    const clone = await sandbox.runCommand('git', ['clone', '--depth', '1', repoUrl, 'repo']);
    if (clone.exitCode !== 0) {
      throw new Error(`Clone failed: ${await clone.stderr()}`);
    }
 
    const ls = await sandbox.runCommand('ls', ['-la', 'repo']);
 
    let readmePreview = '(no README found)';
    const readme = await sandbox.readFileToBuffer({ path: 'repo/README.md' });
    if (readme) {
      readmePreview = readme.toString('utf8').slice(0, 300);
    }
 
    return {
      sandboxName: sandbox.name,
      cloneExitCode: clone.exitCode,
      files: await ls.stdout(),
      readmePreview
    };
  } finally {
    await sandbox.stop();
  }
}

Was this helpful?

supported.