---
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-08-21T16:55:06.986Z"
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.

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.

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

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

```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 `{ sandboxName, cloneExitCode, files, readmePreview }`

## Solution

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


---

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