Swift
Exception Handling
Error Debugging
Programming
Swift Development

How to print details of a 'catch all' exception in Swift?

Master System Design with Codemia

Enhance your system design skills with over 120 practice problems, detailed solutions, and hands-on exercises.

Introduction

Swift error handling is type-safe, but in real apps you still need a fallback when an unexpected error reaches a broad catch. A good catch-all block should log enough detail for debugging without leaking sensitive data. This guide shows a practical pattern for printing rich error details from a catch-all handler.

Catch Order and Error Shape

In Swift, catch blocks are evaluated top to bottom. Put specific errors first, then a final catch-all block for everything else.

swift
1import Foundation
2
3enum NetworkError: Error {
4    case invalidURL
5    case unauthorized
6}
7
8func fetchData(urlString: String) throws -> Data {
9    guard URL(string: urlString) != nil else {
10        throw NetworkError.invalidURL
11    }
12    throw URLError(.timedOut)
13}
14
15do {
16    _ = try fetchData(urlString: "bad url")
17} catch NetworkError.invalidURL {
18    print("NetworkError.invalidURL")
19} catch {
20    print("Unexpected error: \(error)")
21}

The final catch receives an Error value. That is enough for baseline logging, but we can improve it.

Bridge to NSError for Domain and Code

Most Swift and Foundation errors can be bridged to NSError, which exposes domain, code, and userInfo. These fields are valuable during triage.

swift
1import Foundation
2
3func logError(_ error: Error, context: String) {
4    let nsError = error as NSError
5
6    print("Context: \(context)")
7    print("Type: \(type(of: error))")
8    print("Domain: \(nsError.domain)")
9    print("Code: \(nsError.code)")
10    print("Description: \(nsError.localizedDescription)")
11
12    if !nsError.userInfo.isEmpty {
13        print("UserInfo:")
14        for (key, value) in nsError.userInfo {
15            print("  \(key): \(value)")
16        }
17    }
18}
19
20do {
21    _ = try fetchData(urlString: "https://example.com")
22} catch {
23    logError(error, context: "Fetching profile")
24}

This is a production-friendly base because it logs structured details and remains simple.

Add Lightweight Debug Context

Logging raw errors is useful, but context makes logs actionable. Include operation name, user-safe identifiers, and call stack in debug builds.

swift
1import Foundation
2
3func debugLogError(_ error: Error, operation: String, requestID: String) {
4    let nsError = error as NSError
5
6    print("operation=\(operation) request_id=\(requestID)")
7    print("domain=\(nsError.domain) code=\(nsError.code)")
8    print("message=\(nsError.localizedDescription)")
9
10    #if DEBUG
11    let stack = Thread.callStackSymbols.prefix(5)
12    for line in stack {
13        print("stack=\(line)")
14    }
15    #endif
16}

Use this style in development and route it through your logging framework in production.

Differentiate Display Errors and Diagnostic Errors

Users should not see raw diagnostic details. Keep two paths: one for logs and one for UI messaging.

swift
1func userMessage(for error: Error) -> String {
2    switch error {
3    case is URLError:
4        return "The network request failed. Please try again."
5    default:
6        return "Something went wrong."
7    }
8}
9
10func handle(_ error: Error) {
11    logError(error, context: "Checkout flow")
12    let message = userMessage(for: error)
13    print("User-facing: \(message)")
14}

This prevents accidental exposure of internals while still giving developers enough information.

Structured Logging With Logger

Plain print calls are fine for local debugging, but production apps benefit from structured logs. Apple platforms provide Logger through the os module, which integrates with Console and log filtering tools.

swift
1import Foundation
2import os
3
4let logger = Logger(subsystem: "com.example.app", category: "network")
5
6func logWithSystemLogger(_ error: Error, operation: String) {
7    let nsError = error as NSError
8    logger.error("operation=\(operation, privacy: .public) domain=\(nsError.domain, privacy: .public) code=\(nsError.code)")
9    logger.error("message=\(nsError.localizedDescription, privacy: .public)")
10}

Use privacy controls deliberately. Non-sensitive fields can be public, while user data should stay private or redacted. This approach gives better observability without sacrificing safety.

Common Pitfalls

A frequent mistake is only printing localizedDescription and ignoring domain or code, which removes useful grouping signals in logs. Another mistake is catching everything too early and swallowing errors before higher layers can recover. Teams also often dump full userInfo in release logs, which may include sensitive values. If you log it, scrub secrets and tokens first. Finally, avoid relying on crash-only visibility. Structured error logs from catch-all handlers are often the fastest path to root cause.

Summary

  • Put specific catches first, then keep a final catch-all block.
  • Bridge Error to NSError to log domain, code, and userInfo.
  • Attach operation context so logs are actionable.
  • Separate developer diagnostics from user-facing messages.
  • Scrub sensitive values before persisting logs in production.

Course illustration
Course illustration

All Rights Reserved.