Vercel Logo

Wrap the Full Lifecycle in a Function

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

The thing about Sandboxes is that they cost money to run, and they keep running until you stop them. If anything in the middle of our script throws, we leak a Sandbox. So before we hand this off to a CLI in the next section, we're going to wrap the lifecycle in a function with try/finally so stop() runs no matter what.

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 { sandboxId, 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 = {
  sandboxId: string;
  cloneExitCode: number;
  files: string;
  readmePreview: string;
};
 
export async function runSandboxLifecycle(repoUrl: string): Promise<LifecycleResult> {
  const sandbox = await Sandbox.create();
 
  try {
    const clone = await sandbox.runCommand(`git clone ${repoUrl} repo`);
    if (clone.exitCode !== 0) {
      throw new Error(`Clone failed: ${clone.stderr}`);
    }
 
    const ls = await sandbox.runCommand('ls -la repo');
 
    let readmePreview = '(no README found)';
    try {
      const readme = await sandbox.readFile('repo/README.md');
      readmePreview = readme.slice(0, 300);
    } catch {
      // README is optional; preview stays as the default
    }
 
    return {
      sandboxId: sandbox.sandboxId,
      cloneExitCode: clone.exitCode,
      files: 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:

{
  sandboxId: 'sbx_7N2k4A...',
  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 { sandboxId, cloneExitCode, files, readmePreview }

Solution

src/sandbox-lifecycle.ts
import { Sandbox } from '@vercel/sandbox';
 
export type LifecycleResult = {
  sandboxId: string;
  cloneExitCode: number;
  files: string;
  readmePreview: string;
};
 
export async function runSandboxLifecycle(repoUrl: string): Promise<LifecycleResult> {
  const sandbox = await Sandbox.create();
 
  try {
    const clone = await sandbox.runCommand(`git clone ${repoUrl} repo`);
    if (clone.exitCode !== 0) {
      throw new Error(`Clone failed: ${clone.stderr}`);
    }
 
    const ls = await sandbox.runCommand('ls -la repo');
 
    let readmePreview = '(no README found)';
    try {
      const readme = await sandbox.readFile('repo/README.md');
      readmePreview = readme.slice(0, 300);
    } catch {
      // README is optional; preview stays as the default
    }
 
    return {
      sandboxId: sandbox.sandboxId,
      cloneExitCode: clone.exitCode,
      files: ls.stdout,
      readmePreview
    };
  } finally {
    await sandbox.stop();
  }
}

Was this helpful?

supported.