Skip to content

feat(sdk): add throwOnError / raise_on_exception to Sandbox.fork - #2003

Draft
devin-ai-integration[bot] wants to merge 1 commit into
mainfrom
devin/1792507515-fork-throw-on-error
Draft

devin-ai-integration[bot] wants to merge 1 commit into
mainfrom
devin/1792507515-fork-throw-on-error

Conversation

@devin-ai-integration

Copy link
Copy Markdown
Contributor

Summary

Sandbox.fork returns one entry per requested fork, Sandbox | Error (allSettled-style). This adds an opt-in flag that throws instead when any fork failed, carrying all results so the caller can still use or kill the forks that did start.

  • JS: SandboxForkOpts.throwOnError?: boolean → throws SandboxForkError extends SandboxError with results: Array<Sandbox | Error>. Overloads narrow the return type to Sandbox[] (this[] / InstanceType<S>[]) when throwOnError: true.
  • Python (sync + async, static + instance): keyword-only raise_on_exception: bool = False → raises SandboxForkException(SandboxException) with .results. Literal[True] overloads return List[Self] / List["Sandbox"] / List["AsyncSandbox"].
  • Message: "1 of 2 forks failed to start: <per-fork messages joined by '; '>".
  • Default behavior is unchanged. Errors for the whole request (missing source sandbox, auth, …) are thrown the same way as before. The flag is never sent to the API.
try {
  const forks = await sandbox.fork({ count: 3, throwOnError: true }) // Sandbox[]
} catch (e) {
  if (e instanceof SandboxForkError) {
    await Promise.all(e.results.filter((r) => r instanceof Sandbox).map((s) => s.kill()))
  }
}
try:
    forks = sandbox.fork(count=3, raise_on_exception=True)  # List[Sandbox]
except SandboxForkException as e:
    for r in e.results:
        if isinstance(r, Sandbox):
            r.kill()

Tests are offline and mock the fork response (one success plus one 429): packages/js-sdk/tests/sandbox/forkThrowOnError.test.ts and packages/python-sdk/tests/shared/sandbox/test_fork_raise_on_exception.py.

Link to Devin session: https://app.devin.ai/sessions/b5575b1ac20941429b771ed858d33ef6
Open in Devin Desktop: https://app.devin.ai/desktop/session/b5575b1ac20941429b771ed858d33ef6?variant=devin
Requested by: @mishushakov

Co-Authored-By: mish@e2b.dev <mish@e2b.dev>
@devin-ai-integration

Copy link
Copy Markdown
Contributor Author

I'll fix CI failures and address comments from users with write access. I'll skip comments containing "(aside)".

  • Disable automatic comment, CI, and merge conflict monitoring

@cla-bot cla-bot Bot added the cla-signed label Oct 20, 2026
@changeset-bot

changeset-bot Bot commented Oct 20, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: a261290

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 3 packages
Name Type
e2b Minor
@e2b/python-sdk Minor
@e2b/code-interpreter-python Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@linear-code

linear-code Bot commented Oct 20, 2026

Copy link
Copy Markdown

EN-3386

@github-actions

Copy link
Copy Markdown
Contributor

Package Artifacts

Built from 635f44f. Download artifacts from this workflow run.

JS SDK (e2b@2.57.1-devin-1792507515-fork-throw-on-error.0, with @e2b/dockerfile-utils@0.1.2-devin-1792507515-fork-throw-on-error.0):

npm install ./e2b-dockerfile-utils-0.1.2-devin-1792507515-fork-throw-on-error.0.tgz ./e2b-2.57.1-devin-1792507515-fork-throw-on-error.0.tgz

CLI (@e2b/cli@2.22.2-devin-1792507515-fork-throw-on-error.0):

npm install ./e2b-cli-2.22.2-devin-1792507515-fork-throw-on-error.0.tgz

Code Interpreter JS SDK (@e2b/code-interpreter@2.9.1-devin-1792507515-fork-throw-on-error.0):

npm install ./e2b-code-interpreter-2.9.1-devin-1792507515-fork-throw-on-error.0.tgz

Desktop JS SDK (@e2b/desktop@2.5.1-devin-1792507515-fork-throw-on-error.0):

npm install ./e2b-desktop-2.5.1-devin-1792507515-fork-throw-on-error.0.tgz

Python SDK (e2b==2.57.0+devin.1792507515.fork.throw.on.error, with e2b-dockerfile-utils==0.1.1+devin.1792507515.fork.throw.on.error):

pip install ./e2b_dockerfile_utils-0.1.1+devin.1792507515.fork.throw.on.error-py3-none-any.whl ./e2b-2.57.0+devin.1792507515.fork.throw.on.error-py3-none-any.whl

Code Interpreter Python SDK (e2b-code-interpreter==2.11.1+devin.1792507515.fork.throw.on.error):

pip install ./e2b_code_interpreter-2.11.1+devin.1792507515.fork.throw.on.error-py3-none-any.whl

Desktop Python SDK (e2b-desktop==2.7.0+devin.1792507515.fork.throw.on.error):

pip install ./e2b_desktop-2.7.0+devin.1792507515.fork.throw.on.error-py3-none-any.whl

@devin-ai-integration devin-ai-integration Bot left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

TASTE.md review

Checked the changed code against the parity (T-1, T-2, T-10), API shape (T-3/T-3a, T-14, T-23, T-24, T-25), batch semantics (T-33), module structure (T-54), errors (T-57, T-62) and docstring (T-69 to T-72) rules.

3 violations, 6 inline comments:

  1. T-25: the Python fork overloads (sync + async, instance + static) have no bool overload, so a non-literal raise_on_exception doesn't type-check. Inline on the instance overloads; the static overloads at sandbox_sync/main.py:525 and sandbox_async/main.py:530 need the same fix.
  2. T-25 / T-72: in JS, the throwOnError: true overloads kept the old @returns ("a sandbox instance or an error"), which is wrong for that overload.
  3. T-62: the SandboxForkError / SandboxForkException message doesn't tell the user the successful forks are still running.

Not tied to a single line:

  • Same per-variant doc drift in Python (T-25): the Literal[True] overloads repeat the "either a running sandbox or an exception" paragraph, and the Literal[False] overloads carry :raises SandboxForkException: even though that overload can't raise. Each overload's docstring should describe only its own variant.
  • Borderline T-1a / T-10: throwOnError ↔ raise_on_exception differ by more than casing. T-1c only allows the Error/Exception swap for class suffixes, and the strict mirror would be throw_on_error. raise_on_exception reads as idiomatic Python, so this is a call for the maintainers, not a blocker.

Looks right: keyword-only * on the new Python param (T-3a); SandboxForkError extends SandboxError / SandboxForkException(SandboxException) (T-57, T-1c); exported from index.ts and __all__ (T-54); JS overloads narrow the return and keep a non-literal fallback (T-24/T-25); per-item outcomes are still available through .results (T-33); parity across JS, sync and async Python (T-1/T-2).

Written by Devin

@@ -412,17 +417,60 @@ export class Sandbox extends SandboxApi {
* @param opts fork options — `count`, `timeoutMs` and connection options.
*
* @returns array with one entry per requested fork — a sandbox instance or an error.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

T-25 / T-72 — each overload carries its own docs, and @returns must say what this variant returns. This doc block now belongs to the throwOnError: true overload, which resolves to Sandbox[] only, but it still says "a sandbox instance or an error" (and the paragraph above still describes the allSettled-style mixed array). Describe the narrowed return here:

Suggested change
* @returns array with one entry per requested fork — a sandbox instance or an error.
* @returns array of running sandbox instances, one per requested fork, in request order.

@@ -492,17 +549,51 @@ export class Sandbox extends SandboxApi {
* @param opts fork options — `count`, `timeoutMs` and connection options.
*
* @returns array with one entry per requested fork — a sandbox instance or an error.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

T-25 / T-72 — each overload carries its own docs, and @returns must say what this variant returns. This doc block now belongs to the throwOnError: true overload, which resolves to Sandbox[] only, but it still says "a sandbox instance or an error" (and the paragraph above still describes the allSettled-style mixed array). Describe the narrowed return here:

Suggested change
* @returns array with one entry per requested fork — a sandbox instance or an error.
* @returns array of running sandbox instances, one per requested fork, in request order.

timeout: Optional[int] = None,
count: Optional[int] = None,
*,
raise_on_exception: Literal[True],

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

T-25 — a boolean-discriminated pair needs a third overload for the non-literal case. Only Literal[False] and Literal[True] overloads exist, so a real call site such as sandbox.fork(count=2, raise_on_exception=strict) with strict: bool matches neither and pyright/mypy report "no overload matches". (JS gets this right: its opts?: SandboxForkOpts overload takes throwOnError?: boolean.)

Add a bool overload returning the union, with its own docstring, here and on the static fork overload (sandbox_id: str, … at line 525):

    @overload
    def fork(
        self,
        timeout: Optional[int] = None,
        count: Optional[int] = None,
        *,
        raise_on_exception: bool,
        **opts: Unpack[ApiParams],
    ) -> List[Union[Self, Exception]]:
        """<own docstring: may raise SandboxForkException when raise_on_exception is True>"""
        ...

timeout: Optional[int] = None,
count: Optional[int] = None,
*,
raise_on_exception: Literal[True],

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

T-25 — a boolean-discriminated pair needs a third overload for the non-literal case. Only Literal[False] and Literal[True] overloads exist, so a real call site such as sandbox.fork(count=2, raise_on_exception=strict) with strict: bool matches neither and pyright/mypy report "no overload matches". (JS gets this right: its opts?: SandboxForkOpts overload takes throwOnError?: boolean.)

Add a bool overload returning the union, with its own docstring, here and on the static fork overload (sandbox_id: str, … at line 530):

    @overload
    async def fork(
        self,
        timeout: Optional[int] = None,
        count: Optional[int] = None,
        *,
        raise_on_exception: bool,
        **opts: Unpack[ApiParams],
    ) -> List[Union[Self, Exception]]:
        """<own docstring: may raise SandboxForkException when raise_on_exception is True>"""
        ...

Comment on lines +71 to +73
`${failedErrors.length} of ${results.length} forks failed to start: ${failedErrors
.map((error) => error.message)
.join('; ')}`

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

T-62 — error messages are actionable: say what to do, not just what failed. The key thing a user must act on is that the forks that did start keep running (and billing) after this is raised; that is only in the class docstring, not in the message that ends up in logs. Put it in the message:

Suggested change
`${failedErrors.length} of ${results.length} forks failed to start: ${failedErrors
.map((error) => error.message)
.join('; ')}`
`${failedErrors.length} of ${results.length} forks failed to start: ${failedErrors
.map((error) => error.message)
.join('; ')}. The forks that did start are still running — use \`error.results\` to keep or kill them.`

Comment on lines +79 to +80
f"{len(failed_errors)} of {len(results)} forks failed to start: "
f"{'; '.join(str(error) for error in failed_errors)}"

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

T-62 — error messages are actionable: say what to do, not just what failed. The key thing a user must act on is that the forks that did start keep running (and billing) after this is raised; that is only in the class docstring, not in the message that ends up in logs. Put it in the message:

Suggested change
f"{len(failed_errors)} of {len(results)} forks failed to start: "
f"{'; '.join(str(error) for error in failed_errors)}"
f"{len(failed_errors)} of {len(results)} forks failed to start: "
f"{'; '.join(str(error) for error in failed_errors)}. "
"The forks that did start are still running — use `.results` to keep or kill them."

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant