vapor-docs/3.0/docs/async/overview.md

4.5 KiB

Async

Async provides the fundament for asynchronous libraries. It consists of Futures and Streams.

Promise and Future

When working with asynchronous APIs, one of the problems you'll face is not knowing when a variable is set.

When querying a database synchronously, the thread is blocked until a result has been received. At which point the result will be returned to you and the thread continues from where you left off querying the database.

let user = try database.fetchUser(named: "Admin")

print(user.username)

In the asynchronous world, you won't receive a result immediately. Instead, you'll receive a result in a callback.

// Callback `found` will receive the user. If an error occurred, the `onError` callback will be called instead.
try database.fetchUser(named: "Admin", found: { user in
  print(user.username)
}, onError: { error in
  print(error)
})

You can imagine code becoming complex. Difficult to read and comprehend.

Promises and futures are two types that this library introduces to solve this.

Creating a promise

Promises are important if you're implementing a function that returns a result in the future, such as the database shown above.

Promises need to be created without a result. They can then be completed with the expectation or an error at any point.

You can extract a future from the promise that you can hand to the API consumer.

// the example `fetchUser` implementation
func fetchUser(named name: String) -> Future<User> {
	// Creates a promise that can be fulfilled in the future
	let promise = Promise<User>()

	// Run a query asynchronously, looking for the user
	asyncDatabaseQuery(where: "username" == name, onComplete: { databaseResult in
		do {
			// Initialize the user using the datbase result
			// This can throw an error if the result is empty or invalid
			let user = try User(decodingFrom: databaseResult)

			// If initialization is successful, complete the promise.
			//
			// Completing the promise will notify the promise's associated future with this user
			promise.complete(user)
		} catch {
			// If initialization is successful, fail the promise.
			//
			// Failing the promise will notify the promise's associated future with an error
			promise.fail(error)
		}
	})

	// After spawning the asynchronous operation, return the promise's associated future
	//
	// The future can then be used by the API consumer
	return promise.future
}

On future completion

When a promise completes, you can chain the result/error into a closure:

// The future provided by the above function will be used
let future: Future<User> = fetchUser(named: "Admin")

// `.then`'s closure will be executed on success
future.then { user in
  print(user.username)
// `.catch` will catch any error on failure
}.catch { error in
  print(error)
}

Catching specific errors

Sometimes you only care for specific errors, for example, for logging.

// The future provided by the above function will be used
let future: Future<User> = fetchUser(named: "Admin")

// `.then`'s closure will be executed on success
future.then { user in
  print(user.username)
// This `.catch` will only catch `DatabaseError`s
}.catch(DatabaseError.self) { databaseError in
	print(databaseError)
// `.catch` will catch any error on failure, including `DatabaseError` types
}.catch { error in
  print(error)
}

Mapping results

Futures can be mapped to different results asynchronously.

// The future provided by the above function will be used
let future: Future<User> = fetchUser(named: "Admin")

// Maps the user to it's username
let futureUsername: Future<String> = future.map { user in
	return user.username
}

// Mapped futures can be mapped and chained, too
futureUsername.then { username in
	print(username)
}

For synchronous APIs

Sometimes, an API needs to be used synchronously in a synchronous envinronment.

Rather than using a synchronous API with all edge cases involved, we recommend using the try future.sync() function.

// The future provided by the above function will be used
let future: Future<User> = fetchUser(named: "Admin")

// This will either receive the user if the promise was completed or throw an error if the promise was failed.
let user: User = try future.sync()

This will wait for a result indefinitely, blocking the thread.

If you expect a result with a specified duration, say, 30 seconds:

// This will also throw an error if the deadline wasn't met
let user = try future.sync(deadline: .seconds(30))