Skip to main content

Optionals Handling

Overview

Proper handling of optional/nullable values is critical for writing safe, crash-free mobile applications. Each platform has its own approach to null safety, but the principles remain consistent.

Core Principle

Make illegal states unrepresentable. Use the type system to enforce that values are present when required and handle absence explicitly when they're optional.

Swift - Optional Handling

Swift uses Optional<T> (written as T?) to represent values that may be absent.

Rules

  1. Never use force unwrap (!) in production code
  2. Prefer guard let for early exits
  3. Use optional chaining (?.) for safe member access
  4. Use nil coalescing (??) for default values
  5. Use Result or throws for errors, not nil

Early Exit with guard let

Use guard let when you need to exit early if a value is nil.

func render(user: User?) {
guard let user else {
showEmptyState()
return
}

nameLabel.text = user.name
emailLabel.text = user.email
}

If let for Conditional Logic

Use if let when you need different behavior for nil and non-nil cases.

func displayAvatar(url: String?) {
if let url {
imageView.loadImage(from: url)
} else {
imageView.image = UIImage(named: "default-avatar")
}
}

Optional Chaining

Use optional chaining to safely access nested properties.

// GOOD: Safe access with optional chaining
let street = user?.address?.street

// BAD: Multiple force unwraps
let street = user!.address!.street // Will crash if any is nil

Nil Coalescing Operator

Provide default values with ??.

func greet(name: String?) -> String {
let displayName = name ?? "Guest"
return "Hello, \(displayName)!"
}

Multiple Optional Bindings

Swift allows multiple bindings in a single statement.

func calculateDistance(from: Location?, to: Location?) -> Double? {
guard let from, let to else {
return nil
}

return from.distance(to: to)
}

Optional Mapping with map and flatMap

Transform optionals functionally.

// map: transforms Optional<T> -> Optional<U>
let uppercaseName = user?.name.map { $0.uppercased() }

// flatMap: transforms Optional<T> -> Optional<U> when transform returns Optional
let firstCharacter = user?.name.flatMap { $0.first.map(String.init) }

Avoid Nil as Error Signal

Don't use nil to represent errors. Use Result or throws.

// BAD: Nil represents both "not found" and "error"
func fetchUser(id: String) async -> User? {
// Returns nil for both network errors and user not found
}

// GOOD: Explicit error handling
func fetchUser(id: String) async throws -> User {
// Throws specific errors
}

// ALSO GOOD: Result type
func fetchUser(id: String) async -> Result<User, AppError> {
// Returns .success or .failure with specific error
}

Avoid Implicitly Unwrapped Optional

Avoid ! (implicitly unwrapped optionals) except for:

  • IBOutlets (where iOS guarantees initialization)
  • Truly guaranteed initialization patterns
// BAD: Implicit unwrap with no guarantee
class ViewModel {
var repository: Repository! // Might be nil
}

// GOOD: Regular optional or non-optional
class ViewModel {
let repository: Repository

init(repository: Repository) {
self.repository = repository
}
}

Kotlin - Null Safety

Kotlin has built-in null safety with nullable and non-nullable types.

Rules

  1. Avoid !! (force unwrap) unless absolutely necessary
  2. Use safe calls (?.) for nullable access
  3. Use Elvis operator (?:) for defaults
  4. Use let for null-safe transformations
  5. Validate inputs at boundaries

Safe Calls

fun displayUser(user: User?) {
// Safe call - only executes if user is not null
val name = user?.name
val email = user?.email

// Safe call chaining
val street = user?.address?.street
}

Elvis Operator

Provide default values with ?:.

fun greet(name: String?): String {
val displayName = name ?: "Guest"
return "Hello, $displayName!"
}

// Can also be used for early return
fun process(data: String?): Result {
val validData = data ?: return Result.Error("Data is null")
// Process validData
}

Let for Null-Safe Operations

Use let to execute a block only if value is non-null.

fun sendEmail(email: String?) {
email?.let { validEmail ->
emailService.send(validEmail)
}
}

// Multiple nullable values
fun calculateDistance(from: Location?, to: Location?): Double? {
return from?.let { f ->
to?.let { t ->
f.distanceTo(t)
}
}
}

Require and Check

Use require for argument validation and check for state validation.

fun processUser(user: User?) {
requireNotNull(user) { "User must not be null" }

// user is now smart-cast to non-null
println(user.name)
}

class ViewModel {
private var isInitialized = false

fun doWork() {
check(isInitialized) { "ViewModel must be initialized" }
// Proceed with work
}
}

Smart Casts

Kotlin automatically casts after null checks.

fun process(value: String?) {
if (value != null) {
// value is smart-cast to String (non-null)
println(value.length)
}
}

fun process(value: String?) {
when (value) {
null -> println("Value is null")
else -> println(value.length) // Smart-cast to String
}
}

Safe Casts

Use as? for safe casting that returns null on failure.

fun handleData(data: Any) {
val stringData = data as? String
if (stringData != null) {
println("String: $stringData")
}
}

Avoid !! Unless Necessary

Only use !! when you have an invariant guarantee.

// BAD: Forces unwrap with no guarantee
fun process(data: String?) {
val length = data!!.length // Crashes if data is null
}

// GOOD: Handle null explicitly
fun process(data: String?) {
val length = data?.length ?: 0
}

// ACCEPTABLE: When invariant is guaranteed
class Repository(private val apiKey: String?) {
init {
requireNotNull(apiKey) { "API key is required" }
}

fun fetch() {
// apiKey!! is acceptable here because constructor validates
api.fetch(apiKey!!)
}
}

Dart - Null Safety

Dart has sound null safety where non-nullable types cannot be null.

Rules

  1. Prefer non-nullable types by default
  2. Use required for mandatory parameters
  3. Use ? suffix only when value can truly be absent
  4. Avoid ! unless invariant is guaranteed
  5. Use ?? for defaults

Nullable vs Non-Nullable

// Non-nullable - cannot be null
final String name = 'John';
final int age = 30;

// Nullable - can be null
String? middleName;
int? optionalAge;

Required Parameters

Use required for mandatory named parameters.

class User {
final String id;
final String name;
final String? bio; // Optional field

User({
required this.id,
required this.name,
this.bio,
});
}

// Usage
final user = User(
id: '123',
name: 'John',
// bio is optional
);

Null-Aware Operators

// Null-aware access (?.)
String? name = user?.name;

// Null-aware method call
user?.save();

// Null coalescing (??)
String displayName = user?.name ?? 'Guest';

// Null-aware assignment (??=)
String? cachedValue;
cachedValue ??= fetchFromNetwork(); // Only assigns if null

Safe Cascades

// Regular cascade (requires non-null)
user
..name = 'John'
..email = 'john@example.com';

// Null-aware cascade (skips the whole cascade if user is null)
user
?..name = 'John'
..email = 'john@example.com';

Late Variables

Use late for non-nullable fields initialized after construction.

class ViewModel {
late final Repository repository;

void init(Repository repo) {
repository = repo; // Must be assigned before use
}

void fetch() {
repository.getData(); // Crashes if not initialized
}
}

Null Assertion (!)

Only use ! when you have a guarantee.

// BAD: No guarantee
void process(String? data) {
print(data!.length); // Crashes if data is null
}

// GOOD: Handle null explicitly
void process(String? data) {
if (data != null) {
print(data.length); // data is promoted to non-null
}
}

// ACCEPTABLE: Guaranteed by design
class Config {
String? _apiKey;

void initialize(String key) {
_apiKey = key;
}

String get apiKey {
assert(_apiKey != null, 'Config must be initialized');
return _apiKey!; // OK because of assert
}
}

Type Promotion

Dart automatically promotes nullable types after null checks.

void greet(String? name) {
if (name == null) {
print('Hello, Guest!');
return;
}

// name is promoted to String (non-null) here
print('Hello, ${name.toUpperCase()}!');
}

Pattern Matching (Dart 3.0+)

String greet(String? name) {
return switch (name) {
null => 'Hello, Guest!',
String n => 'Hello, $n!',
};
}

Common Patterns Across Platforms

Validating Input at Boundaries

Always validate external input (API responses, user input, deep links).

// iOS
func handle(deepLink: URL?) -> DeepLinkAction? {
guard let deepLink else { return nil }
guard deepLink.scheme == "myapp" else { return nil }
guard let host = deepLink.host else { return nil }

return DeepLinkAction(host: host)
}
// Android
fun handleDeepLink(uri: Uri?): DeepLinkAction? {
val validUri = uri ?: return null
if (validUri.scheme != "myapp") return null
val host = validUri.host ?: return null

return DeepLinkAction(host)
}
// Flutter
DeepLinkAction? handleDeepLink(Uri? uri) {
if (uri == null) return null;
if (uri.scheme != 'myapp') return null;
final host = uri.host;
if (host.isEmpty) return null;

return DeepLinkAction(host: host);
}

Optional Collections

Prefer empty collections over nullable collections.

// GOOD: Non-nullable with default empty
var items: [Item] = []

// BAD: Nullable collection
var items: [Item]?
// GOOD
val items: List<Item> = emptyList()

// BAD
val items: List<Item>? = null
// GOOD
final List<Item> items = <Item>[];

// BAD
List<Item>? items;

Factory Methods for Safe Construction

// iOS
extension User {
static func from(dto: UserDTO?) -> User? {
guard let dto else { return nil }
guard let id = dto.id, !id.isEmpty else { return nil }
guard let name = dto.name, !name.isEmpty else { return nil }

return User(id: id, name: name, email: dto.email)
}
}
// Android
fun UserDTO?.toDomain(): User? {
this ?: return null
if (id.isNullOrBlank()) return null
if (name.isNullOrBlank()) return null

return User(id, name, email)
}
// Flutter
extension UserDTOExtension on UserDTO? {
User? toDomain() {
final dto = this;
if (dto == null) return null;
if (dto.id?.isEmpty ?? true) return null;
if (dto.name?.isEmpty ?? true) return null;

return User(id: dto.id!, name: dto.name!, email: dto.email);
}
}

Summary

PlatformNullable SyntaxSafe AccessDefault ValueForce Unwrap
SwiftT??.??! (avoid)
KotlinT??.?:!! (avoid)
DartT??.??! (avoid)

Golden Rule: Avoid force unwrap operators unless you have a documented invariant that guarantees the value is present.