Overview
Result.gen() enables imperative-style error handling using JavaScript generators and yield* syntax:
Write code that looks synchronous but short-circuits on first error
Automatic error type union inference across multiple yields
Support for finally blocks, using declarations, and async operations
Type-safe alternative to try-catch and promise chaining
Basic Usage
Simple Composition
const getA = () : Result < number , string > => Result . ok ( 1 );
const getB = ( a : number ) : Result < number , string > => Result . ok ( a + 1 );
const getC = ( b : number ) : Result < number , string > => Result . ok ( b + 1 );
const result = Result . gen ( function* () {
const a = yield * getA (); // Unwraps Ok(1) -> a = 1
const b = yield * getB ( a ); // Unwraps Ok(2) -> b = 2
const c = yield * getC ( b ); // Unwraps Ok(3) -> c = 3
return Result . ok ( c );
});
// Result<number, string> = Ok(3)
Short-Circuiting on Error
When any yield* encounters an Err, execution stops immediately:
class ErrorA extends TaggedError ( "ErrorA" )<{ message : string }>() {}
class ErrorB extends TaggedError ( "ErrorB" )<{ message : string }>() {}
const result = Result . gen ( function* () {
const a = yield * Result . ok ( 1 ); // a = 1
const b = yield * Result . err ( new ErrorA ({ message: "failed" })); // Stops here!
const c = yield * Result . ok ( 3 ); // Never executed
return Result . ok ( a + b + c );
});
// Result<number, ErrorA> = Err(ErrorA)
This is railway-oriented programming : operations proceed on the “success track” until an error switches to the “error track”.
How It Works
The yield* Operator
yield* delegates to the iterator protocol. Ok and Err implement [Symbol.iterator]:
// From result.ts
class Ok < A , E > {
* [ Symbol . iterator ]() : Generator < Err < never , E >, A , unknown > {
return this . value ; // Immediately returns without yielding
}
}
class Err < T , E > {
* [ Symbol . iterator ]() : Generator < Err < never , E >, never , unknown > {
yield this as unknown as Err < never , E >; // Yields error, stops execution
return panic ( "Unreachable" , this . error );
}
}
When Result.gen() encounters a yielded Err, it stops the generator and returns that error.
Type Inference
TypeScript infers the union of all yielded error types:
class ErrorA extends TaggedError ( "ErrorA" )<{ message : string }>() {}
class ErrorB extends TaggedError ( "ErrorB" )<{ message : string }>() {}
class ErrorC extends TaggedError ( "ErrorC" )<{ message : string }>() {}
const getA = () : Result < number , ErrorA > => Result . ok ( 1 );
const getB = () : Result < number , ErrorB > => Result . ok ( 2 );
const getC = () : Result < number , ErrorC > => Result . ok ( 3 );
const result = Result . gen ( function* () {
const a = yield * getA (); // Can fail with ErrorA
const b = yield * getB (); // Can fail with ErrorB
const c = yield * getC (); // Can fail with ErrorC
return Result . ok ( a + b + c );
});
// Result<number, ErrorA | ErrorB | ErrorC>
Comparison to Other Patterns
vs Try-Catch
Try-Catch (throws)
Result.gen (type-safe)
try {
const user = await fetchUser ( id );
const validated = await validateUser ( user );
const saved = await saveUser ( validated );
return saved ;
} catch ( error ) {
// All errors lumped together
// No type safety on error
console . error ( error );
throw error ;
}
vs Promise Chaining
Promise Chaining
Result.gen
fetchUser ( id )
. then ( user => validateUser ( user ))
. then ( validated => saveUser ( validated ))
. then ( saved => {
console . log ( "Success:" , saved );
return saved ;
})
. catch ( error => {
console . error ( "Error:" , error );
throw error ;
});
vs andThen Chaining
andThen (functional)
Result.gen (imperative)
fetchUser ( id )
. andThen ( user => validateUser ( user ))
. andThen ( validated => saveUser ( validated ))
. tap ( saved => console . log ( "Success:" , saved ));
Use andThen() for simple linear chains. Use Result.gen() when you need:
Multiple intermediate values
Conditional logic
Loops or complex control flow
Resource cleanup with finally
Async Generators
Result.await()
Wrap Promise<Result> to make it yieldable in async generators:
const fetchUser = ( id : string ) : Promise < Result < User , NotFoundError >> => {
// ...
};
const result = await Result . gen ( async function* () {
const user = yield * Result . await ( fetchUser ( "123" ));
const profile = yield * Result . await ( fetchProfile ( user . id ));
return Result . ok ({ user , profile });
});
Mixing Sync and Async
const result = await Result . gen ( async function* () {
// Sync operation
const validated = yield * validateInput ( input );
// Async operation
const user = yield * Result . await ( fetchUser ( validated . id ));
// Another sync operation
const enriched = yield * enrichUserData ( user );
return Result . ok ( enriched );
});
Error Propagation
Automatic Error Union
Errors from all yields are automatically unioned:
class NotFoundError extends TaggedError ( "NotFoundError" )<{
id : string ;
message : string ;
}>() {}
class ValidationError extends TaggedError ( "ValidationError" )<{
field : string ;
message : string ;
}>() {}
class DatabaseError extends TaggedError ( "DatabaseError" )<{
message : string ;
cause : unknown ;
}>() {}
const processUser = ( id : string ) : Result < User , NotFoundError | ValidationError | DatabaseError > => {
return Result . gen ( function* () {
const user = yield * fetchUser ( id ); // NotFoundError
const validated = yield * validateUser ( user ); // ValidationError
const saved = yield * saveUser ( validated ); // DatabaseError
return Result . ok ( saved );
});
};
Normalizing Error Types
Use mapError() to convert error unions to a single type:
class AppError extends TaggedError ( "AppError" )<{
message : string ;
originalTag : string ;
}>() {}
const processUser = ( id : string ) : Result < User , AppError > => {
return Result . gen ( function* () {
const user = yield * fetchUser ( id );
const validated = yield * validateUser ( user );
const saved = yield * saveUser ( validated );
return Result . ok ( saved );
}). mapError ( e => new AppError ({
message: e . message ,
originalTag: e . _tag ,
}));
};
Resource Cleanup
Finally Blocks
finally blocks run even when short-circuiting:
const result = Result . gen ( function* () {
let resource : Resource | null = null ;
try {
resource = yield * acquireResource ();
const data = yield * processResource ( resource );
return Result . ok ( data );
} finally {
if ( resource ) {
resource . cleanup ();
}
}
});
If a finally block throws, Result.gen() will throw a Panic. Ensure cleanup code doesn’t throw: finally {
// Bad: might throw
resource . cleanup ();
// Good: catch errors
try {
resource . cleanup ();
} catch ( e ) {
console . error ( "Cleanup failed:" , e );
}
}
Using Declarations (Resource Management)
TC39 Explicit Resource Management (Stage 3) works with Result.gen():
const result = Result . gen ( function* () {
using resource = yield * acquireResource ();
// resource implements Symbol.dispose
const data = yield * processResource ( resource );
// resource.cleanup() called automatically, even on error
return Result . ok ( data );
});
Async Resource Management
const result = await Result . gen ( async function* () {
await using connection = yield * Result . await ( connectToDatabase ());
// connection implements Symbol.asyncDispose
const user = yield * Result . await ( fetchUser ( connection , id ));
// connection.close() called automatically
return Result . ok ( user );
});
Complex Control Flow
Conditional Logic
const processOrder = ( orderId : string ) : Result < Order , AppError > => {
return Result . gen ( function* () {
const order = yield * fetchOrder ( orderId );
if ( order . status === "pending" ) {
const validated = yield * validateOrder ( order );
const processed = yield * processPayment ( validated );
return Result . ok ( processed );
} else if ( order . status === "shipped" ) {
return Result . err ( new OrderAlreadyShippedError ({ orderId }));
} else {
const refunded = yield * refundOrder ( order );
return Result . ok ( refunded );
}
});
};
Loops
const processItems = (
items : string []
) : Result < ProcessedItem [], ProcessError > => {
return Result . gen ( function* () {
const results : ProcessedItem [] = [];
for ( const item of items ) {
const validated = yield * validateItem ( item );
const processed = yield * processItem ( validated );
results . push ( processed );
}
return Result . ok ( results );
});
};
If any yield* in the loop fails, the entire operation stops and returns that error.
Early Returns
const updateUser = (
id : string ,
data : UserUpdate
) : Result < User , AppError > => {
return Result . gen ( function* () {
const user = yield * fetchUser ( id );
// Early return on condition
if ( user . deleted ) {
return Result . err ( new UserDeletedError ({ id }));
}
if ( ! user . emailVerified && data . email ) {
return Result . err ( new EmailNotVerifiedError ({ id }));
}
const updated = yield * saveUser ({ ... user , ... data });
return Result . ok ( updated );
});
};
Context Binding
Bind this context with the second parameter:
class UserService {
constructor ( private db : Database ) {}
async processUser ( id : string ) : Promise < Result < User , AppError >> {
return Result . gen ( async function* ( this : UserService ) {
const user = yield * Result . await ( this . db . fetchUser ( id ));
const validated = yield * this . validateUser ( user );
return Result . ok ( validated );
}, this );
}
validateUser ( user : User ) : Result < User , ValidationError > {
// ...
}
}
Real-World Examples
User Registration Flow
class ValidationError extends TaggedError ( "ValidationError" )<{
field : string ;
message : string ;
}>() {}
class DuplicateError extends TaggedError ( "DuplicateError" )<{
email : string ;
message : string ;
}>() {}
class EmailError extends TaggedError ( "EmailError" )<{
message : string ;
cause : unknown ;
}>() {}
type RegistrationError = ValidationError | DuplicateError | EmailError ;
const registerUser = async (
input : RegistrationInput
) : Promise < Result < User , RegistrationError >> => {
return Result . gen ( async function* () {
// Validate input
const validated = yield * validateInput ( input );
// Check for existing user
const existing = yield * Result . await ( findUserByEmail ( validated . email ));
if ( existing . isOk ()) {
return Result . err ( new DuplicateError ({
email: validated . email ,
message: "Email already registered" ,
}));
}
// Hash password
const hashedPassword = yield * hashPassword ( validated . password );
// Create user
const user = yield * Result . await (
createUser ({ ... validated , password: hashedPassword })
);
// Send welcome email (don't fail registration if this fails)
const emailResult = yield * Result . await (
sendWelcomeEmail ( user . email )
);
if ( emailResult . isErr ()) {
console . warn ( "Failed to send welcome email:" , emailResult . error );
}
return Result . ok ( user );
});
};
Batch Processing with Rollback
const processBatch = async (
items : Item []
) : Promise < Result < void , BatchError >> => {
return Result . gen ( async function* () {
const processed : ProcessedItem [] = [];
try {
for ( const item of items ) {
const validated = yield * validateItem ( item );
const result = yield * Result . await ( processItem ( validated ));
processed . push ( result );
}
// Commit all
yield * Result . await ( commitBatch ( processed ));
return Result . ok ( undefined );
} catch ( error ) {
// Rollback processed items
for ( const item of processed . reverse ()) {
await rollbackItem ( item ). catch ( console . error );
}
throw error ;
}
});
};
Multi-Step Workflow
const processOrderWorkflow = async (
orderId : string
) : Promise < Result < Receipt , WorkflowError >> => {
return Result . gen ( async function* () {
// Step 1: Fetch order
console . log ( "[1/5] Fetching order..." );
const order = yield * Result . await ( fetchOrder ( orderId ));
// Step 2: Validate inventory
console . log ( "[2/5] Validating inventory..." );
const inventory = yield * Result . await ( checkInventory ( order . items ));
// Step 3: Reserve items
console . log ( "[3/5] Reserving items..." );
using reservation = yield * Result . await ( reserveItems ( inventory ));
// Step 4: Process payment
console . log ( "[4/5] Processing payment..." );
const payment = yield * Result . await ( processPayment ( order . total ));
// Step 5: Generate receipt
console . log ( "[5/5] Generating receipt..." );
const receipt = yield * generateReceipt ( order , payment );
console . log ( "✓ Order processed successfully" );
return Result . ok ( receipt );
});
};
Best Practices
Always return Result from generator
The generator body must return Result.ok() or Result.err(): // Good
Result . gen ( function* () {
const x = yield * getX ();
return Result . ok ( x * 2 );
});
// Bad: returns number directly
Result . gen ( function* () {
const x = yield * getX ();
return x * 2 ; // Panic!
});
Don't throw in generators
Throwing before any yield* will cause a Panic: // Bad: throws before yielding
Result . gen ( function* () {
throw new Error ( "oops" ); // Panic!
});
// Good: return error
Result . gen ( function* () {
return Result . err ( new MyError ({ ... }));
});
Ensure finally blocks don't throw
Wrap cleanup in try-catch: Result . gen ( function* () {
try {
const resource = yield * acquire ();
return Result . ok ( resource );
} finally {
try {
cleanup ();
} catch ( e ) {
console . error ( "Cleanup failed:" , e );
}
}
});
Use Result.await for promises
Always wrap Promise<Result> with Result.await(): // Good
const user = yield * Result . await ( fetchUser ( id ));
// Bad: yields Promise, not Result
const user = yield * fetchUser ( id ); // Type error
Summary
Use Result.gen() when you need:
Imperative style - Code that reads like normal sync/async code
Multiple values - Access to intermediate results
Complex control flow - Conditions, loops, early returns
Resource cleanup - finally blocks or using declarations
Error union - Automatic inference of all possible error types
Avoid Result.gen() when:
Simple linear transformations (use map()/andThen())
Purely functional composition (use pipeable API)
No intermediate values needed
Next Steps
Creating Results Learn how to create Result instances with ok, err, try, and tryPromise
Error Handling Master TaggedError and exhaustive error matching