Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,6 +1,21 @@
import Flow
import Foundation

/// Application-specific errors for wizard operations.
enum WizardError: Error, LocalizedError {
case validationFailed(step: String, errors: [String])
case submissionFailed(underlying: Error)

var errorDescription: String? {
switch self {
case .validationFailed(let step, let errors):
return "Validation failed in \(step): \(errors.joined(separator: ", "))"
case .submissionFailed(let underlying):
return "Submission failed: \(underlying.localizedDescription)"
}
}
}

/// Multi-step wizard/form with complex validation and navigation.
///
/// Demonstrates:
Expand Down Expand Up @@ -39,7 +54,7 @@ struct MultiStepWizardFeature: Feature {
// UI state
var isValidating = false
var isSubmitting = false
var error: FlowError?
var error: WizardError?

// Validation errors per step
var personalInfoErrors: [String] = []
Expand Down Expand Up @@ -219,15 +234,15 @@ struct MultiStepWizardFeature: Feature {
// Reset wizard after successful submission
resetWizard(state: state)
} catch {
throw FlowError.networkError(underlying: error)
throw WizardError.submissionFailed(underlying: error)
}
}
.catch { error, state in
state.isSubmitting = false
if let vfError = error as? FlowError {
state.error = vfError
if let wizardError = error as? WizardError {
state.error = wizardError
} else {
state.error = .networkError(underlying: error)
state.error = .submissionFailed(underlying: error)
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,27 @@
import Flow
import Foundation

/// Application-specific errors for retry networking.
enum RetryError: Error, LocalizedError {
case maxRetriesExceeded(attempts: Int)
case networkFailure(underlying: Error)

var errorDescription: String? {
switch self {
case .maxRetriesExceeded(let attempts):
return "Failed after \(attempts) retry attempts"
case .networkFailure(let underlying):
return "Network error: \(underlying.localizedDescription)"
}
}
}

/// Advanced networking feature with exponential backoff retry logic.
///
/// Demonstrates:
/// - Retry with exponential backoff
/// - Maximum retry attempts
/// - Error handling with FlowError
/// - Error handling with custom error types
/// - Task cancellation
/// - Loading states during retries
struct RetryNetworkFeature: Feature {
Expand All @@ -32,7 +47,7 @@ struct RetryNetworkFeature: Feature {
final class State {
var data: [Item]?
var isLoading = false
var error: FlowError?
var error: RetryError?

// Retry tracking
var retryCount = 0
Expand All @@ -41,7 +56,7 @@ struct RetryNetworkFeature: Feature {
init(
data: [Item]? = nil,
isLoading: Bool = false,
error: FlowError? = nil
error: RetryError? = nil
) {
self.data = data
self.isLoading = isLoading
Expand Down Expand Up @@ -73,9 +88,7 @@ struct RetryNetworkFeature: Feature {

case .retryFetch:
guard state.retryCount < maxRetries else {
state.error = .custom(
message: "Maximum retry attempts (\(maxRetries)) exceeded"
)
state.error = .maxRetriesExceeded(attempts: maxRetries)
state.isLoading = false
return .none
}
Expand Down Expand Up @@ -121,7 +134,7 @@ struct RetryNetworkFeature: Feature {
)
} else {
// Max retries exceeded
state.error = .networkError(underlying: error)
state.error = .networkFailure(underlying: error)
return .none
}
}
Expand Down Expand Up @@ -152,20 +165,17 @@ struct RetryNetworkFeature: Feature {
// Retry recursively
try await performFetch(state: state)
} else {
throw FlowError.custom(
message: "Failed after \(maxRetries) retries",
underlying: error
)
throw RetryError.maxRetriesExceeded(attempts: maxRetries)
}
}
}
.cancellable(id: "fetch-data", cancelInFlight: true)
.catch { error, state in
state.isLoading = false
if let vfError = error as? FlowError {
state.error = vfError
if let retryError = error as? RetryError {
state.error = retryError
} else {
state.error = .networkError(underlying: error)
state.error = .networkFailure(underlying: error)
}
}
}
Expand Down
44 changes: 28 additions & 16 deletions Examples/AdvancedPatterns/PaginatedList/PaginatedListFeature.swift
Original file line number Diff line number Diff line change
@@ -1,6 +1,18 @@
import Flow
import Foundation

/// Application-specific errors for pagination operations.
enum PaginationError: Error, LocalizedError {
case fetchFailed(underlying: Error)

var errorDescription: String? {
switch self {
case .fetchFailed(let underlying):
return "Failed to fetch items: \(underlying.localizedDescription)"
}
}
}

/// Paginated list feature with infinite scrolling.
///
/// Demonstrates:
Expand Down Expand Up @@ -28,7 +40,7 @@ struct PaginatedListFeature: Feature {
var nextCursor: String? // Pagination cursor
var hasMore = true // More pages available

var error: FlowError?
var error: PaginationError?

init(
items: [Item] = [],
Expand Down Expand Up @@ -72,16 +84,16 @@ struct PaginatedListFeature: Feature {
state.isInitialLoading = false
state.error = nil
} catch {
throw FlowError.networkError(underlying: error)
throw PaginationError.fetchFailed(underlying: error)
}
}
.cancellable(id: "fetch-page", cancelInFlight: true)
.catch { error, state in
state.isInitialLoading = false
if let vfError = error as? FlowError {
state.error = vfError
if let paginationError = error as? PaginationError {
state.error = paginationError
} else {
state.error = .networkError(underlying: error)
state.error = .fetchFailed(underlying: error)
}
}

Expand All @@ -102,16 +114,16 @@ struct PaginatedListFeature: Feature {
state.isRefreshing = false
state.error = nil
} catch {
throw FlowError.networkError(underlying: error)
throw PaginationError.fetchFailed(underlying: error)
}
}
.cancellable(id: "fetch-page", cancelInFlight: true)
.catch { error, state in
state.isRefreshing = false
if let vfError = error as? FlowError {
state.error = vfError
if let paginationError = error as? PaginationError {
state.error = paginationError
} else {
state.error = .networkError(underlying: error)
state.error = .fetchFailed(underlying: error)
}
}

Expand Down Expand Up @@ -139,16 +151,16 @@ struct PaginatedListFeature: Feature {
state.isLoadingMore = false
state.error = nil
} catch {
throw FlowError.networkError(underlying: error)
throw PaginationError.fetchFailed(underlying: error)
}
}
.cancellable(id: "fetch-page", cancelInFlight: true)
.catch { error, state in
state.isLoadingMore = false
if let vfError = error as? FlowError {
state.error = vfError
if let paginationError = error as? PaginationError {
state.error = paginationError
} else {
state.error = .networkError(underlying: error)
state.error = .fetchFailed(underlying: error)
}
}

Expand All @@ -165,10 +177,10 @@ struct PaginatedListFeature: Feature {
state.isRefreshing = false
state.isLoadingMore = false

if let vfError = error as? FlowError {
state.error = vfError
if let paginationError = error as? PaginationError {
state.error = paginationError
} else {
state.error = .networkError(underlying: error)
state.error = .fetchFailed(underlying: error)
}

return .none
Expand Down
32 changes: 24 additions & 8 deletions Examples/AdvancedPatterns/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
**学習内容:**
- 指数バックオフリトライ
- 最大リトライ回数の制御
- FlowError によるエラーハンドリング
- カスタムエラー型によるエラーハンドリング
- タスクキャンセル
- リトライ中のローディング状態

Expand All @@ -36,7 +36,7 @@ return .run { state in
try await Task.sleep(for: delay)
try await performFetch(state: state) // 再帰的リトライ
} else {
throw FlowError.custom(message: "Max retries exceeded")
throw RetryError.maxRetriesExceeded(attempts: maxRetries)
}
}
}
Expand Down Expand Up @@ -215,14 +215,30 @@ struct RobustPaginatedListFeature: Feature {

### 1. エラーハンドリング

全ての例で FlowError を使用
各機能で専用のエラー型を定義してエラーハンドリング

```swift
// アプリケーション固有のエラー型を定義
enum RetryError: Error, LocalizedError {
case maxRetriesExceeded(attempts: Int)
case networkFailure(underlying: Error)

var errorDescription: String? {
switch self {
case .maxRetriesExceeded(let attempts):
return "Failed after \(attempts) retry attempts"
case .networkFailure(let underlying):
return "Network error: \(underlying.localizedDescription)"
}
}
}

// エラーハンドリング
.catch { error, state in
if let vfError = error as? FlowError {
state.error = vfError
if let retryError = error as? RetryError {
state.error = retryError
} else {
state.error = .networkError(underlying: error)
state.error = .networkFailure(underlying: error)
}
}
```
Expand Down Expand Up @@ -260,7 +276,7 @@ return .run { state in
@Observable
final class State {
var isLoading = false
var error: FlowError?
var error: RetryError? // アプリケーション固有のエラー型
}

// ❌ Bad: Feature に状態を持つ
Expand Down Expand Up @@ -290,7 +306,7 @@ swift test --filter MultiStepWizardFeature
/// Demonstrates:
/// - Retry with exponential backoff
/// - Maximum retry attempts
/// - Error handling with FlowError
/// - Error handling with custom error types
/// ...
struct RetryNetworkFeature: Feature { ... }
```
Expand Down
6 changes: 3 additions & 3 deletions Sources/Flow/Store/ActionTask.swift
Original file line number Diff line number Diff line change
Expand Up @@ -482,7 +482,7 @@ extension ActionTask {

/// Concatenates an array of tasks to run sequentially.
///
/// Throws `FlowError.noTasksToExecute` if the task array is empty.
/// Throws `StoreError.noTasksToExecute` if the task array is empty.
/// This strict behavior helps catch logic errors early.
///
/// ## Example: Dynamic Tasks with Guard
Expand All @@ -507,10 +507,10 @@ extension ActionTask {
///
/// - Parameter tasks: Array of tasks to concatenate (must not be empty)
/// - Returns: A single task that runs all tasks sequentially
/// - Throws: `FlowError.noTasksToExecute` if tasks array is empty
/// - Throws: `StoreError.noTasksToExecute` if tasks array is empty
public static func concatenate(_ tasks: [ActionTask]) throws -> ActionTask {
guard let first = tasks.first else {
throw FlowError.noTasksToExecute(context: "concatenate(_:)")
throw StoreError.noTasksToExecute(context: "concatenate(_:)")
}
// TCA-style reduce pattern implementing Monoid
return tasks.dropFirst().reduce(first) { $0.concatenate(with: $1) }
Expand Down
Loading
Loading