Migrating from @swan-io/boxed to neverthrow
temporal-contract previously used @swan-io/boxed for its Result/Future pattern. Starting in this major version it uses neverthrow instead — the shape of the API surface is the same (signals/queries/updates/activities all return a Result-like value), but the names and instance methods are different.
This page is an end-to-end mapping for upgrading existing code.
Why the change
- Ecosystem:
neverthrowis the most widely-used Result library on npm and is a more familiar API for new contributors. - Bundle size:
neverthrowships smaller bundles and avoids the customFutureclass — neverthrow'sResultAsynccovers the same use cases. - Maintenance:
@swan-io/boxedhad a critical issue affecting our usage that prompted the move.
Drop the dep, add the new one
// package.json
"dependencies": {
- "@swan-io/boxed": "^3"
+ "neverthrow": "^8"
}If you used @temporal-contract/boxed, that package no longer exists — its exports are gone and replaced by neverthrow types. Remove every import.
- import { Future, Result } from "@swan-io/boxed";
- import { Future, Result } from "@temporal-contract/boxed";
+ import { ResultAsync, ok, err, okAsync, errAsync } from "neverthrow";Type signatures
Every Future<Result<T, E>> becomes ResultAsync<T, E>. Bare Future<T> (no Result) is not used anywhere in the public surface, so the migration is a 1-for-1 rename.
- (args: TInput): Future<Result<TOutput, MyError>>
+ (args: TInput): ResultAsync<TOutput, MyError>Result construction
| boxed | neverthrow |
|---|---|
Result.Ok(value) | ok(value) |
Result.Error(error) | err(error) |
Future.value(Result.Ok(value)) | okAsync(value) |
Future.value(Result.Error(error)) | errAsync(error) |
Type-guards & accessors
- if (result.isError()) { console.log(result.error); }
+ if (result.isErr()) { console.log(result.error); }
// .isOk() and the .value / .error accessors after narrowing are unchanged.Method renames
| boxed | neverthrow |
|---|---|
.flatMap(fn) | .andThen(fn) |
.flatMapOk(fn) | .andThen(fn) |
.flatMapError(fn) | .orElse(fn) |
.mapError(fn) | .mapErr(fn) |
.getOr(default) | .unwrapOr(default) |
.match({ Ok, Error }) | .match(okFn, errFn) — positional, not object form |
Result.fromExecution(fn) | Result.fromThrowable(fn)() |
Result.fromAsyncExecution(fn) | ResultAsync.fromPromise(fn(), e => e) |
Result.all([...]) | Result.combine([...]) |
tap / tapOk / tapError have no direct replacement
Inline the side-effect into a .map(...) (or .mapErr(...)) that returns the value unchanged:
- result.tapOk((value) => logger.info("processed", value));
+ result.map((value) => {
+ logger.info("processed", value);
+ return value;
+ });Promise interop
Future.fromPromise(promise, mapError) becomes ResultAsync.fromPromise(promise, mapError):
- const f = Future.fromPromise(api.fetch(id), (e) => new MyError(e));
+ const f = ResultAsync.fromPromise(api.fetch(id), (e) => new MyError(e));If you don't need to map the error type, .fromPromise requires the second argument anyway — pass (e) => e or a (e) => e as Error cast.
Awaiting a ResultAsync
ResultAsync<T, E> is awaitable; await resolves to Result<T, E>. This matches Future<Result<T, E>> from boxed — no change to call sites that already await the value before checking it.
const result = await contractClient.executeWorkflow("processOrder", { ... });
if (result.isErr()) {
// result.error: WorkflowFailedError | RuntimeClientError | ...
}End-to-end activity example
Before (boxed):
import { Future, Result } from "@swan-io/boxed";
import { ApplicationFailure, declareActivitiesHandler } from "@temporal-contract/worker/activity";
export const activities = declareActivitiesHandler({
contract,
activities: {
sendEmail: ({ to, subject }) => {
return Future.fromPromise(emailService.send(to, subject)).mapError((e) =>
ApplicationFailure.create({
type: "EMAIL_FAILED",
message: e instanceof Error ? e.message : "Failed",
cause: e instanceof Error ? e : undefined,
}),
);
},
},
});After (neverthrow):
import { ResultAsync } from "neverthrow";
import { ApplicationFailure, declareActivitiesHandler } from "@temporal-contract/worker/activity";
export const activities = declareActivitiesHandler({
contract,
activities: {
sendEmail: ({ to, subject }) =>
ResultAsync.fromPromise(emailService.send(to, subject), (e) =>
ApplicationFailure.create({
type: "EMAIL_FAILED",
message: e instanceof Error ? e.message : "Failed",
cause: e instanceof Error ? e : undefined,
}),
),
},
});End-to-end client example
Before (boxed):
const result = await client.executeWorkflow("processOrder", { workflowId, args });
result.match({
Ok: (output) => console.log("Order:", output),
Error: (err) => console.error("Failed:", err),
});After (neverthrow):
const result = await client.executeWorkflow("processOrder", { workflowId, args });
result.match(
(output) => console.log("Order:", output),
(err) => console.error("Failed:", err),
);Cancellation scopes
context.cancellableScope and context.nonCancellableScope previously returned Future<Result<T, WorkflowCancelledError>>. They now return ResultAsync<T, WorkflowCancelledError> — the only consumer change is .isError() → .isErr().