stent state. This pattern is the only native way to create truly divergent paths in a promise chain without introducing external state or helper wrappers.
Core Solution
To implement divergent control flow, you must leverage the two-argument signature of .then() to attach isolated handlers to a specific promise. This approach decouples the success path from the rejection path, ensuring that errors in the success handler do not bleed into the rejection handler.
Technical Implementation
- Identify the Source Promise: Determine the asynchronous operation that requires a fallback.
- Define Isolated Handlers: Create a function for the success path and a function for the rejection path. Ensure both return compatible types if the chain continues.
- Apply Dual Arguments: Pass both handlers to
.then().
- Verify Convergence: Understand that after the
.then() call, the chain converges. The next handler receives the result of whichever branch executed.
Code Example: Feature Flag Loading
Consider a scenario where you load a feature flag. If the load succeeds, you apply the theme. If the load fails, you fall back to a system default. If applying the theme fails, you want the error to propagate, not trigger the system default fallback.
interface ThemeConfig {
mode: 'dark' | 'light';
accent: string;
}
declare function fetchFeatureFlag(flagId: string): Promise<ThemeConfig>;
declare function applyTheme(config: ThemeConfig): void;
declare function useSystemDefault(): ThemeConfig;
declare function renderUI(theme: ThemeConfig): void;
// Divergent Path Implementation
fetchFeatureFlag('ui_theme')
.then(
(config) => {
// Success Path: Only runs if fetchFeatureFlag resolves.
// If applyTheme throws, this error propagates down the chain.
applyTheme(config);
return config;
},
(error) => {
// Rejection Path: Only runs if fetchFeatureFlag rejects.
// This handler is isolated from errors in applyTheme.
console.warn('Feature flag load failed, using system default.', error);
return useSystemDefault();
}
)
.then((finalTheme) => {
// Convergence: Runs regardless of which path was taken.
// Receives the ThemeConfig from either the success or fallback branch.
renderUI(finalTheme);
})
.catch((criticalError) => {
// Global Error Handler: Catches errors from fetchFeatureFlag,
// applyTheme, or useSystemDefault.
// Note: useSystemDefault errors are caught here, not in the fallback above.
reportToMonitoring(criticalError);
});
Architecture Decisions
- Isolation vs. Recovery: Choose
.then(success, fallback) when the fallback is an alternative to the source operation. Choose .catch() when you need to recover from failures in the source or the handler.
- Return Value Consistency: Both handlers in the two-argument
.then() should ideally return values of the same type. If success returns ThemeConfig and fallback returns undefined, the subsequent chain must handle a union type, increasing complexity.
- Error Re-throwing: If the rejection handler cannot resolve the error, it must throw or return a rejected promise. Returning a value converts the rejection to a resolution, which may hide critical failures if not intended.
Pitfall Guide
1. The "Catch-All" Fallacy
Explanation: Developers assume .then(success, fallback) catches errors thrown inside success. This is false. The fallback handler is only triggered by rejections from the promise preceding the .then() call.
Fix: If you need to catch errors from success, chain a .catch() after the .then(), or restructure the logic. Use .then(success, fallback) strictly for source isolation.
2. Silent Fallback Failures
Explanation: If the rejection handler returns undefined or a value that breaks downstream expectations, the chain resolves successfully with that value. This can mask errors where the fallback itself failed silently.
Fix: Ensure the rejection handler returns a valid state or explicitly throws if it cannot recover. Validate return types in TypeScript to prevent undefined leakage.
3. Async/Await Translation Errors
Explanation: When converting .then(success, fallback) to async/await, developers often write:
try {
const result = await source();
return success(result);
} catch (e) {
return fallback(e);
}
This pattern catches errors from success(result) as well, breaking isolation.
Fix: Use a helper that preserves isolation, or separate the await:
const result = await source().catch(fallback);
return success(result);
Or use a dedicated utility function (see Production Bundle).
4. The Convergence Trap
Explanation: Developers sometimes expect the chain to stop after the rejection handler executes. However, .then() always returns a new promise. The next .then() in the chain will execute regardless of which handler ran, receiving the result of that handler.
Fix: Design the chain with convergence in mind. If the fallback produces a value, the next handler must be prepared to process it. If the fallback throws, the next .then() is skipped, and the error propagates to the next .catch().
5. Non-Function Arguments
Explanation: Passing a non-function (e.g., null, undefined, or a string) to the second argument causes the engine to insert a thrower function. This re-throws the error, effectively acting as if no handler was provided.
Fix: Always pass a function or explicitly pass undefined if you want the error to propagate. Avoid passing falsy values that might be mistaken for handlers.
6. Overusing Divergence for Simple Errors
Explanation: Using .then(success, fallback) for every error case can make chains harder to read compared to a clear .catch() block, especially when the fallback logic is complex.
Fix: Reserve the two-argument pattern for cases where isolation is semantically required. For general error handling, .catch() remains the standard and more readable choice.
Production Bundle
Action Checklist
Decision Matrix
| Scenario | Recommended Approach | Why | Cost Impact |
|---|
| Primary action with strict fallback | .then(success, fallback) | Isolates fallback from success errors; prevents redundant fallback execution. | Low |
| Recovery from any error in chain | .then(success).catch(fallback) | Catches errors from both source and success handler; ensures robust recovery. | Low |
| Transform error to value | .then(undefined, transform) | Maps rejection to resolution without affecting success path. | Low |
| Complex async/await flow | withFallback helper | Maintains isolation in imperative syntax; reduces boilerplate. | Medium (helper maintenance) |
| Multiple fallback strategies | Nested .then() or Promise.allSettled | Allows granular control over multiple error sources. | High |
Configuration Template
Use this TypeScript utility to replicate the isolation behavior of .then(success, fallback) in async/await functions. This helper ensures that errors in the success callback do not trigger the fallback.
/**
* Executes a promise with an isolated fallback handler.
* The fallback is only triggered if the promise rejects.
* Errors thrown by the success callback are NOT caught by the fallback.
*
* @param promise - The source promise.
* @param success - Handler for resolved value.
* @param fallback - Handler for rejection reason.
* @returns Promise resolving to the result of success or fallback.
*/
export function withIsolatedFallback<T, R>(
promise: Promise<T>,
success: (value: T) => R,
fallback: (reason: unknown) => R
): Promise<R> {
return promise.then(
(value) => success(value),
(reason) => fallback(reason)
);
}
// Usage in async function:
async function loadConfig(): Promise<Config> {
return withIsolatedFallback(
fetchRemoteConfig(),
(remote) => parseRemoteConfig(remote),
(err) => {
console.error('Remote config failed, using local.', err);
return getLocalConfig();
}
);
}
Quick Start Guide
- Identify the Source: Locate the promise that requires a fallback mechanism.
- Define Handlers: Write the success handler and the fallback handler. Ensure they return the same type.
- Apply Pattern: Replace
.then(success).catch(fallback) with .then(success, fallback) if isolation is required.
- Validate: Run tests to confirm that errors in the success handler do not trigger the fallback.
- Integrate: Continue the chain with subsequent handlers that process the converged result.