---
title: "Wrap the Lifecycle"
description: "Turn the script we've been growing into a proper reusable function. Accept a repo URL as input, wrap the body in try/finally so the Sandbox always stops, and return a structured result that the rest of the course can consume."
canonical_url: "https://vercel.com/academy/vercel-sandbox/wrap-the-lifecycle"
md_url: "https://vercel.com/academy/vercel-sandbox/wrap-the-lifecycle.md"
docset_id: "vercel-academy"
doc_version: "1.0"
last_updated: "2026-05-17T04:39:04.378Z"
content_type: "lesson"
course: "vercel-sandbox"
course_title: "Vercel Sandbox"
prerequisites:  []
---

<agent-instructions>
Vercel Academy — structured learning, not reference docs.
Lessons are sequenced.
Adapt commands to the human's actual environment (OS, package manager, shell, editor) — detect from project context or ask, don't assume.
The lesson shows one path; if the human's project diverges, adapt concepts to their setup.
Preserve the learning goal over literal steps.
Quizzes are pedagogical — engage, don't spoil.
Quiz answers are included for your reference.
</agent-instructions>

# Wrap the Lifecycle

# 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.

```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();
  }
}
```

Two changes worth pointing out. First, a failed clone now `throw`s 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):

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

main();
```

\*\*Warning: 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.

\*\*Note: 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

```bash
pnpm tsx src/sandbox-lifecycle.ts
```

Expected output:

```txt
{
  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

```bash
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

```ts title="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();
  }
}
```


---

[Full course index](/academy/llms.txt) · [Sitemap](/academy/sitemap.md)
