Result Type — một kiểu container đại diện cho kết quả của một hoạt động có thể hoàn thành với thành công (Success) hoặc thất bại (Failure). Không giống như exception, Result truyền lỗi như một giá trị thông thường mà không cần unwinding stack, giúp việc xử lý lỗi dự kiến trở nên an toàn hơn và có thể kết hợp được. Theo Apple Swift Documentation (2026), Result<Success, Failure> trong Swift cho phép chuỗi các hoạt động với xử lý lỗi tự động qua map và flatMap mà không làm gián đoạn thực thi chương trình.
Những điểm chính
Result Type là một kiểu generic bao bọc kết quả của một hoạt động có thể thành công hoặc thất bại. Không giống như exception, nơi lỗi làm gián đoạn luồng bình thường và yêu cầu unwinding stack để tìm khối catch, Result truyền lỗi như một giá trị thông thường — người gọi luôn nhận được một đối tượng và tự quyết định cách xử lý. Điều này đặc biệt hữu ích cho các lỗi dự kiến: đầu vào không hợp lệ, quy tắc kinh doanh, từ chối máy chủ, nơi exception sẽ là cơ chế quá nặng.
Khái niệm Result bắt nguồn từ lập trình hàm, nơi các kiểu tương tự được gọi là Either (hoặc Left/Right). Trong Swift, Result trở thành kiểu chuẩn trong Swift 5.0; trong Kotlin, Result<T> xuất hiện trong thư viện chuẩn; trong Dart 3.0, Result<T> tích hợp được giới thiệu. Mỗi triển khai cung cấp các phương thức làm việc với container: map (biến đổi giá trị thành công), flatMap (chuỗi các hàm Result), mapError (biến đổi lỗi), fold (xử lý cả hai trường hợp). Result Type là nền tảng của xử lý lỗi hàm trong phát triển di động, một giải pháp thay thế cho try-catch trong các tình huống dự kiến.
Lợi thế chính của Result là khả năng kết hợp. Bạn có thể kết hợp nhiều hoạt động, mỗi hoạt động có thể thất bại, thành một chuỗi duy nhất không có khối try-catch lồng nhau. Nếu bất kỳ hoạt động nào trong chuỗi thất bại với Failure, toàn bộ chuỗi bị gián đoạn và trả về Failure — không cần một điều kiện if hay khối catch nào. Điều này làm cho mã trở nên tuyến tính và dễ đọc, đặc biệt trong các tình huống có nhiều yêu cầu API tuần tự hoặc kiểm tra quy tắc kinh doanh.
Trong Swift, Result<Success, Failure> là một enum với hai trường hợp: .success(Success) và .failure(Failure), trong đó Failure bị ràng buộc bởi giao thức Error. Result trong Swift là một kiểu hàm đầy đủ với các phương thức map, flatMap, mapError và get. get là một phương thức đặc biệt: nó trả về giá trị Success nếu kết quả thành công và ném lỗi nếu là Failure. Điều này cho phép Result hoạt động như cầu nối giữa phong cách hàm và phong cách dựa trên exception: xử lý qua map/flatMap trong chuỗi và cuối cùng sử dụng get với do-catch để tích hợp với mã throws.
Result trong Swift là một enum với tham số generic, cho phép trình biên dịch kiểm tra xử lý toàn diện qua switch hoặc do-catch. Nếu bạn thêm một trường hợp mới vào enum NetworkError, trình biên dịch sẽ tạo lỗi trong tất cả biểu thức switch nơi trường hợp đó không được xử lý. Kiểm tra toàn diện là lợi thế chính của Result so với exception: trình biên dịch đảm bảo tất cả lỗi có thể đã được tính đến tại thời điểm biên dịch. Ngược lại, exception không được trình biên dịch kiểm tra trong Swift (chỉ throws được khai báo, nhưng không phải kiểu lỗi).
enum NetworkError: Error {
case badURL
case requestFailed(String)
case decodingFailed
}
func fetchUser(id: Int) -> Result<User, NetworkError> {
guard let url = URL(string: "https://api.example.com/users/\(id)") else {
return .failure(.badURL)
}
let result = performRequest(url: url)
switch result {
case let .success(data):
if let user = try? JSONDecoder().decode(User.self, from: data) {
return .success(user)
}
return .failure(.decodingFailed)
case let .failure(error):
return .failure(.requestFailed(error.localizedDescription))
}
}
// Sử dụng với switch
let result = fetchUser(id: 42)
switch result {
case .success(let user):
showUser(user)
case .failure(.badURL):
logError("Invalid URL")
case .failure(.requestFailed(let msg)):
showAlert(msg)
case .failure(.decodingFailed):
logError("Decoding error")
}
Result<Success, Failure> cho phép định kiểu lỗi ở cấp độ kiểu: hàm fetchUser trả về Result<User, NetworkError>, trong đó NetworkError là một enum cụ thể với ba trường hợp. Người gọi xử lý từng trường hợp qua switch với phạm vi bao phủ toàn diện — trình biên dịch kiểm tra rằng tất cả biến thể đã được xử lý. Switch toàn diện là một lợi thế chính của Result so với exception: trình biên dịch đảm bảo bạn không quên xử lý .badURL, .requestFailed hoặc .decodingFailed. Với exception, trình biên dịch không yêu cầu xử lý và việc thiếu catch(.badURL) có thể dễ dàng bị bỏ qua trong đánh giá mã.
Trong Kotlin, Result<T> là một kiểu tích hợp từ thư viện chuẩn đại diện cho thành công (T) hoặc lỗi (Throwable). Không giống Swift, Result trong Kotlin không cho phép chỉ định kiểu lỗi cụ thể — chỉ Throwable. Điều này được thực hiện vì đơn giản nhưng yêu cầu kiểm tra thêm kiểu lỗi qua biểu thức when. Result trong Kotlin hỗ trợ các hàm fold (xử lý cả hai trường hợp), getOrNull (thành công hoặc null), getOrDefault (thành công hoặc giá trị mặc định), map, recover, andThen. Một đặc điểm của Kotlin: Result không được thiết kế để truyền trực tiếp qua ranh giới hàm — nó không thể được sử dụng làm kiểu trả về cho các phương thức Android SDK hoặc API Kotlin Coroutines mà không có điều chỉnh bổ sung.
fun parseJson(input: String): Result<JsonObject> {
return runCatching {
JsonParser.parseString(input).asJsonObject()
}
}
fun validateEmail(email: String): Result<String> {
return if (email.contains("@")) {
Result.success(email.trim())
} else {
Result.failure(IllegalArgumentException("Invalid email"))
}
}
data class SignupData(val name: String, val email: String)
fun processSignup(name: String, email: String): SignupResult {
return validateEmail(email).fold(
onSuccess = { validateName(name) },
onFailure = { SignupResult.Error("Invalid email") }
)
}
runCatching là một wrapper tiện lợi trong Kotlin bắt bất kỳ exception nào và trả về Result.failure. validateEmail trả về Result.success hoặc Result.failure tùy theo xác thực. fold xử lý cả hai trường hợp một cách gọn nhẹ: khi Success, hàm xác thực tiếp theo được gọi; khi Failure, SignupResult.Error được trả về. Quan trọng: Result trong Kotlin không được thiết kế để lưu trữ trong trường data class hoặc truyền trực tiếp qua ranh giới hàm suspend — sử dụng sealed class tùy chỉnh (Success/Error/Loading) để đại diện cho trạng thái UI trong Jetpack Compose hoặc MVVM.
Từ Dart 3.0, thư viện chuẩn bao gồm Result<T> tích hợp — một sealed class với hai hàm tạo: T.ok() (thành công) và Error.error() (lỗi với Object và StackTrace). Trước Dart 3.0, các nhà phát triển Flutter sử dụng Either<L, R> từ gói dartz hoặc sealed class tùy chỉnh. Result tích hợp trong Dart là tối giản: nó không cung cấp map/flatMap trực tiếp — các hàm này cần được triển khai qua when hoặc sử dụng phương thức mở rộng. Đối với xử lý hàm nghiêm túc, Either từ fpdart vẫn là giải pháp mạnh mẽ hơn với hỗ trợ map, flatMap, mapLeft, fold, andThen và toán tử bind.
Either<L, R> là một kiểu lệch trái từ gói fpdart, trong đó Left là lỗi và Right là thành công. Không giống Result<T> tích hợp, Either định kiểu lỗi ở cấp tham số kiểu (L), cho phép phân biệt kiểu lỗi tại thời điểm biên dịch. Gói fpdart cung cấp một bộ đầy đủ các combinator hàm: map (Right -> Right), mapLeft (Left -> Left), flatMap (bind — Either lồng nhau), andThen (chuỗi không biến đổi), fold (thoát khỏi Either), getOrElse (giá trị mặc định). Đối với ứng dụng Flutter với cách tiếp cận hàm, Either là tiêu chuẩn thực tế.
import 'dart:convert';
import 'package:fpdart/fpdart.dart';
class UserService {
Either<AppError, User> fetchUser(String id) {
try {
final response = await http.get(
Uri.parse('https://api.example.com/users/$id')
);
if (response.statusCode == 200) {
final user = User.fromJson(
json.decode(response.body)
);
return Either.of(user);
}
return Either.left(
AppError.serverError(response.statusCode)
);
} on SocketException catch (e) {
return Either.left(AppError.networkError(e.message));
}
}
}
// Sử dụng với fold
final result = await service.fetchUser('42');
result.fold(
(left) => showError(left.message),
(right) => showUser(right),
);
Either<L, R> từ fpdart là kiểu lệch trái: Left — lỗi, Right — thành công. fetchUser trả về Either<AppError, User>, trong đó AppError là sealed class với các kiểu lỗi cụ thể (serverError, networkError). fold xử lý cả hai trường hợp: callback đầu tiên cho Left (lỗi), callback thứ hai cho Right (thành công). Trong Dart 3.0, Result tích hợp cũng hỗ trợ fold, nhưng không cung cấp map/flatMap. Đối với chuỗi, Either từ fpdart cung cấp map, flatMap (bind), mapLeft, andThen — một bộ đầy đủ các combinator hàm cho việc kết hợp lỗi.
Lợi thế chính của Result so với exception là khả năng kết hợp. Nếu bạn có nhiều hoạt động, mỗi hoạt động có thể thất bại, bạn có thể chuỗi chúng bằng map và flatMap mà không cần một if lồng nhau hay try-catch nào. map biến đổi giá trị thành công: Result.success(x) -> Result.success(f(x)). flatMap (còn gọi là bind hoặc andThen) dành cho trường hợp bản thân biến đổi trả về Result: Result.success(x) -> f(x) -> Result<Y>. Nếu bất kỳ bước nào thất bại với Failure, các hoạt động tiếp theo không được thực thi — chuỗi bị gián đoạn.
data class UserRequest(val userId: String, val token: String)
sealed class AuthError {
data object InvalidToken : AuthError()
data class UserNotFound(val id: String) : AuthError()
}
typealias Outcome<T> = Either<AuthError, T>
fun validateToken(token: String): Outcome<String> =
if (token.isNotBlank()) Either.right(token)
else Either.left(AuthError.InvalidToken)
fun fetchProfile(userId: String): Outcome<Profile> =
if (userId == "42") Either.right(Profile("Alice"))
else Either.left(AuthError.UserNotFound(userId))
// Kết hợp qua flatMap (andThen trong fpdart)
val result = validateToken("abc123")
.flatMap { fetchProfile("42") }
.map { it.name }
.getOrElse { "Guest" }
println(result) // "Alice"
Chuỗi: validateToken -> fetchProfile -> map name -> getOrElse “Khách”. Nếu validateToken trả về Left (InvalidToken), chuỗi bị gián đoạn và trả về “Khách”. Nếu fetchProfile trả về Left (UserNotFound) — cũng “Khách”. Nếu cả hai hoạt động thành công — tên hồ sơ. flatMap cho phép kết hợp các hàm Either, mỗi hàm có thể thất bại, thành một chuỗi tuyến tính duy nhất. Trong phong cách exception truyền thống, cùng một mã sẽ yêu cầu hai try-catch lồng nhau hoặc kiểm tra null. getOrElse ở cuối là điểm thoát khỏi sự kết hợp, cung cấp giá trị mặc định cho trường hợp Failure.
Result và exception không phải là các cách tiếp cận loại trừ lẫn nhau. Mỗi loại có lĩnh vực riêng và trong một ứng dụng di động được thiết kế tốt, cả hai đều được sử dụng. Sự lựa chọn phụ thuộc vào việc lỗi là dự kiến hay không dự kiến. Result dành cho lỗi dự kiến là một phần của logic kinh doanh: email không hợp lệ, không đủ tiền, vượt quá giới hạn tốc độ. Exception dành cho lỗi hệ thống không dự kiến: mất mạng, OutOfMemoryError, NullPointerException (những lỗi không nên xảy ra nhưng vẫn xảy ra).
| Tiêu chí | Result Type | Exception (Exception/Error) |
|---|---|---|
| Loại lỗi | Dự kiến (logic kinh doanh) | Không dự kiến (hệ thống) |
| Hiệu suất | Chi phí thấp (không unwinding stack) | Chi phí cao (unwinding stack, thu thập StackTrace) |
| Kết hợp | Qua map/flatMap — chuỗi tuyến tính | Try-catch lồng nhau — khó đọc |
| Trình biên dịch | Kiểm tra toàn diện (switch/when) | Chỉ checked exception trong Java |
| Luồng thực thi | Không bị gián đoạn — lỗi như giá trị | Bị gián đoạn đến catch gần nhất |
| Kiểm thử | Dễ: kiểm tra kết quả, xác nhận isSuccess/isError | Yêu cầu assertThrows và đối tượng mock |
| Khi nào sử dụng | Xác thực kinh doanh, chuỗi yêu cầu, biểu mẫu | Mất mạng, lỗi I/O, sập hệ thống |
Quy tắc thực tế: nếu lỗi là một phần của luồng công việc ứng dụng bình thường (người dùng nhập email không hợp lệ, không đủ quyền) — sử dụng Result. Nếu lỗi là tình huống đặc biệt (máy chủ không phản hồi, hết bộ nhớ) — sử dụng exception. Trong phát triển di động, Result tại ranh giới lớp (UseCase -> ViewModel) và exception bên trong lớp (API -> Repository) là một mẫu phổ biến kết hợp lợi thế của cả hai cách tiếp cận.
Việc di chuyển mã hiện có dựa trên exception sang Result nên được thực hiện dần dần. Bắt đầu với ranh giới lớp: bọc các lời gọi hàm throws trong Result { try ... } (Swift) hoặc runCatching { ... } (Kotlin). Sau đó thay thế kiểu trả về của các phương thức Repository và UseCase bằng Result/Either, giữ nguyên triển khai nội bộ trên exception. Ở bước cuối cùng, di chuyển ViewModel: thay vì UiState với exception, sử dụng sealed class UiState<T> (Loading, Success, Error), trong đó Error lưu trữ lỗi miền, không phải Throwable. Việc di chuyển dần dần cho phép kiểm thử từng lớp riêng biệt mà không cần tái cấu trúc toàn bộ.
Câu hỏi thường gặp
Optional (T?) đại diện cho sự hiện diện hoặc vắng mặt của một giá trị — nil có nghĩa là “không có dữ liệu” nhưng không giải thích lý do. Result (Success/Failure) chứa không chỉ thành công mà còn lý do lỗi với kiểu cụ thể. Sử dụng Optional khi sự vắng mặt là bình thường (ví dụ: trường hồ sơ tùy chọn) và Result khi bạn cần thông tin lỗi.
Trong Swift, sử dụng Result { try throwingFunc() } — hàm tạo Result nhận một closure throws. Trong Kotlin, sử dụng runCatching { throwingFunc() }, trả về Result<T>. Trong Dart, sử dụng Result<T>.tryCatch(() => throwingFunc()). Điều này cho phép tích hợp dễ dàng mã dựa trên exception vào chuỗi Result.
Sử dụng mapError (Swift) hoặc mapLeft (Either trong Dart/Kotlin) để biến đổi kiểu lỗi mà không thay đổi giá trị thành công. Nếu bạn cần xử lý cả hai trường hợp và trả về một giá trị duy nhất, sử dụng fold. Để ghi log mà không làm gián đoạn chuỗi, sử dụng onFailure (Kotlin) hoặc điểm kiểm tra tiền tố.
Có, nhưng cẩn thận. Result<T> trong Kotlin không được khuyến nghị làm kiểu trả về trực tiếp cho hàm suspend do đặc thù của trình biên dịch K2 và phản chiếu. Sử dụng sealed class tùy chỉnh NetworkResult<T> (Success, Error, Loading) để đại diện cho trạng thái trong coroutines. Đối với lỗi dự kiến trong logic kinh doanh, Either từ Arrow là một giải pháp thay thế mạnh mẽ hơn.
fold là một phương thức nhận hai callback: onSuccess (cho trường hợp thành công) và onFailure (cho trường hợp lỗi), và trả về một giá trị duy nhất thuộc bất kỳ kiểu nào. Nó tương đương với biểu thức switch nhưng dưới dạng hàm bậc cao. fold là điểm thoát chính từ chuỗi Result, nơi bạn chuyển đổi Success/Failure thành UiState, chuỗi cho người dùng hoặc một Result khác.
Tóm tắt
Chúng tôi sẽ phát triển ứng dụng di động chìa khóa trao tay
IT Sectr tạo các ứng dụng iOS và Android cho các công ty khởi nghiệp và doanh nghiệp từ năm 2017. Chúng tôi sẽ tư vấn và đề xuất giải pháp tốt nhất cho bạn.
Đọc thêm