Przejdź do treści

Errors

Vapor builds on Swift's Error protocol for error handling. Route handlers can either throw an error or return a failed EventLoopFuture. Throwing or returning a Swift Error will result in a 500 status response and the error will be logged. AbortError and DebuggableError can be used to change the resulting response and logging respectively. The handling of errors is done by ErrorMiddleware. This middleware is added to the application by default and can be replaced with custom logic if desired.

Abort

Vapor provides a default error struct named Abort. This struct conforms to both AbortError and DebuggableError. You can initialize it with an HTTP status and optional failure reason.

// 404 error, default "Not Found" reason used.
throw Abort(.notFound)

// 401 error, custom reason used.
throw Abort(.unauthorized, reason: "Invalid Credentials")

In old asynchronous situations where throwing is not supported and you must return an EventLoopFuture, like in a flatMap closure, you can return a failed future.

guard let user = user else {
    req.eventLoop.makeFailedFuture(Abort(.notFound))    
}
return user.save()

Vapor includes a helper extension for unwrapping futures with optional values: unwrap(or:).

User.find(id, on: db)
    .unwrap(or: Abort(.notFound))
    .flatMap 
{ user in
    // Non-optional User supplied to closure.
}

If User.find returns nil, the future will be failed with the supplied error. Otherwise, the flatMap will be supplied with a non-optional value. If using async/await then you can handle optionals as normal:

guard let user = try await User.find(id, on: db) {
    throw Abort(.notFound)
}

Abort Error

By default, any Swift Error thrown or returned by a route closure will result in a 500 Internal Server Error response. When built in debug mode, ErrorMiddleware will include a description of the error. This is stripped out for security reasons when the project is built in release mode.

To configure the resulting HTTP response status or reason for a particular error, conform it to AbortError.

import Vapor

enum MyError {
    case userNotLoggedIn
    case invalidEmail(String)
}

extension MyError: AbortError {
    var reason: String {
        switch self {
        case .userNotLoggedIn:
            return "User is not logged in."
        case .invalidEmail(let email):
            return "Email address is not valid: \(email)."
        }
    }

    var status: HTTPStatus {
        switch self {
        case .userNotLoggedIn:
            return .unauthorized
        case .invalidEmail:
            return .badRequest
        }
    }
}

Debuggable Error

ErrorMiddleware uses the Logger.report(error:) method for logging errors thrown by your routes. This method will check for conformance to protocols like CustomStringConvertible and LocalizedError to log readable messages.

To customize error logging, you can conform your errors to DebuggableError. This protocol includes a number of helpful properties like a unique identifier, source location, and stack trace. Most of these properties are optional which makes adopting the conformance easy.

To best conform to DebuggableError, your error should be a struct so that it can store source and stack trace information if needed. Below is an example of the aforementioned MyError enum updated to use a struct and capture error source information.

import Vapor

struct MyError: DebuggableError {
    enum Value {
        case userNotLoggedIn
        case invalidEmail(String)
    }

    var identifier: String {
        switch self.value {
        case .userNotLoggedIn:
            return "userNotLoggedIn"
        case .invalidEmail:
            return "invalidEmail"
        }
    }

    var reason: String {
        switch self.value {
        case .userNotLoggedIn:
            return "User is not logged in."
        case .invalidEmail(let email):
            return "Email address is not valid: \(email)."
        }
    }

    var value: Value
    var source: ErrorSource?

    init(
        _ value: Value,
        file: String = #file,
        function: String = #function,
        line: UInt = #line,
        column: UInt = #column
    ) {
        self.value = value
        self.source = .init(
            file: file,
            function: function,
            line: line,
            column: column
        )
    }
}

DebuggableError has several other properties like possibleCauses and suggestedFixes that you can use to improve the debuggability of your errors. Take a look at the protocol itself for more information.

Stack Traces

Vapor includes support for viewing stack traces for both normal Swift errors and crashes.

Swift Backtrace

Vapor uses the SwiftBacktrace library to provide stack traces after a fatal error or assertion on Linux. In order for this to work, your app must include debug symbols during compilation.

swift build -c release -Xswiftc -g

Error Traces

By default, Abort will capture the current stack trace when initialized. Your custom error types can achieve this by conforming to DebuggableError and storing StackTrace.capture().

import Vapor

struct MyError: DebuggableError {
    var identifier: String
    var reason: String
    var stackTrace: StackTrace?

    init(
        identifier: String,
        reason: String,
        stackTrace: StackTrace? = .capture()
    ) {
        self.identifier = identifier
        self.reason = reason
        self.stackTrace = stackTrace
    }
}

When your application's log level is set to .debug or lower, error stack traces will be included in log output.

Stack traces will not be captured when the log level is greater than .debug. To override this behavior, set StackTrace.isCaptureEnabled manually in configure.

// Always capture stack traces, regardless of log level.
StackTrace.isCaptureEnabled = true

Error Middleware

ErrorMiddleware is one of the only two middlewares added to your application by default. This middleware converts Swift errors that have been thrown or returned by your route handlers into HTTP responses. Without this middleware, errors thrown will result in the connection being closed without a response.

To customize error handling beyond what AbortError and DebuggableError provide, you can replace ErrorMiddleware with your own error handling logic. To do this, first remove the default error middleware by manually initializing app.middleware. Then, add your own error handling middleware as the first middleware to your application.

// Clear all default middleware (then, add back route logging)
app.middleware = .init()
app.middleware.use(RouteLoggingMiddleware(logLevel: .info))
// Add custom error handling middleware first.
app.middleware.use(MyErrorMiddleware())

Very few middleware should go before the error handling middleware. A notable exception to this rule is CORSMiddleware.