# What Are ASIO's I/O Objects and How Are They Used: A Complete Guide

> Learn about ASIO's I/O objects, which wrap asynchronous resources like sockets and timers. Understand how they bind resources to an executor for event loop completion.

- Repository: [chriskohlhoff/asio](https://github.com/chriskohlhoff/asio)
- Tags: deep-dive
- Published: 2026-07-12

---

**ASIO's I/O objects are class templates that wrap asynchronous resources—including sockets, timers, and acceptors—and bind them to an executor that dispatches completion handlers through an event loop.**

ASIO (the standalone version of Boost.Asio maintained in the `chriskohlhoff/asio` repository) models every asynchronous resource as an **I/O object**. These objects serve as the primary interface between your application code and the underlying operating system resources, providing a consistent API for initiating asynchronous operations and handling completions.

## Core Architecture of ASIO I/O Objects

The architecture relies on a clear separation between the user-facing object, the executor that schedules work, and the service that performs the actual I/O.

### The basic_io_object Base Class

All I/O objects inherit (directly or indirectly) from `basic_io_object`, defined in [`include/asio/basic_io_object.hpp`](https://github.com/chriskohlhoff/asio/blob/main/include/asio/basic_io_object.hpp) at lines 53-56. Although this base class is deprecated, it remains the foundation for the current implementation, storing the underlying implementation and providing access to the associated `io_context`.

### Executor Association with any_io_executor

Every I/O object has an `executor_type` typedef, defaulting to `any_io_executor`. This polymorphic executor, defined in [`include/asio/any_io_executor.hpp`](https://github.com/chriskohlhoff/asio/blob/main/include/asio/any_io_executor.hpp) at lines 37-40, satisfies the property set required by any I/O object and allows generic code to work with different executors—whether from a thread pool, `io_context`, or custom implementations.

As shown in [`include/asio/basic_socket.hpp`](https://github.com/chriskohlhoff/asio/blob/main/include/asio/basic_socket.hpp) at lines 74-76, the executor controls how completion handlers are dispatched. When you construct an I/O object with an `io_context`, the object captures the context's executor, ensuring that all callbacks run on that executor's event loop.

### The io_context as the Core I/O Service

The `io_context` class, declared in [`include/asio/io_context.hpp`](https://github.com/chriskohlhoff/asio/blob/main/include/asio/io_context.hpp) at lines 66-73, owns the low-level services that perform I/O (such as the socket service and timer service). It also supplies the default executor for all I/O objects created from it. The `io_context` represents the central hub where asynchronous operations are registered and where their completions are processed.

## Common Types of I/O Objects in ASIO

ASIO provides specialized I/O objects for different asynchronous resources:

- **`ip::tcp::socket`** – Stream-oriented TCP socket for reliable, bidirectional communication.
- **`ip::udp::socket`** – Datagram socket for connectionless UDP communication.
- **`ip::tcp::acceptor`** – Listening socket that accepts incoming TCP connections.
- **`steady_timer`** and **`deadline_timer`** – Timer objects for time-based asynchronous events, with `deadline_timer` defined in [`include/asio/basic_deadline_timer.hpp`](https://github.com/chriskohlhoff/asio/blob/main/include/asio/basic_deadline_timer.hpp).
- **`basic_pipe`** – Pipe handle for inter-process communication on supported platforms.

Each of these types follows the same pattern: they inherit the base characteristics, expose an executor, and provide asynchronous member functions that initiate operations through the underlying service.

## How to Use ASIO I/O Objects

Working with I/O objects follows a consistent lifecycle across all resource types.

### 1. Construction with an Executor

An I/O object must be constructed with either an explicit executor or an `io_context` (from which the executor is obtained).

```cpp
asio::io_context ctx;
asio::ip::tcp::socket sock(ctx);  // Uses ctx's executor

// Or with an explicit executor
asio::any_io_executor exec = ctx.get_executor();
asio::steady_timer timer(exec);

```

### 2. Opening and Binding

Many objects, particularly sockets, must be opened or bound before use. Constructor overloads in [`basic_socket.hpp`](https://github.com/chriskohlhoff/asio/blob/main/basic_socket.hpp) can open the resource automatically, or you can call `open()` and `bind()` explicitly after construction.

### 3. Initiating Asynchronous Operations

The object provides `async_*` member functions that accept completion tokens or handlers. Internally, these functions forward the request to the underlying service (socket service, timer service, etc.) and register the handler with the associated executor.

```cpp
sock.async_connect(endpoint,
    [&](const asio::error_code& ec) {
        if (!ec) std::cout << "connected!\n";
    });

```

### 4. Running the Event Loop

The `io_context` (or any executor driving the I/O objects) must be run to process pending asynchronous operations.

```cpp
ctx.run();  // Blocks until all work is finished

```

### 5. Work Tracking for Composed Operations

When composing operations using `asio::compose` or `asio::co_spawn`, additional I/O objects can be passed to keep their work alive. This is why many APIs accept a variadic `io_objects_or_executors` parameter, as referenced in [`include/asio/compose.hpp`](https://github.com/chriskohlhoff/asio/blob/main/include/asio/compose.hpp) at line 41. The executor ensures that the objects remain valid until all asynchronous operations complete.

## Practical Code Examples

### TCP Echo Server

This example demonstrates the core I/O objects: `io_context`, `ip::tcp::acceptor`, and `ip::tcp::socket`.

```cpp
#include <asio.hpp>

int main()
{
    // 1. Core I/O object – io_context
    asio::io_context ctx;

    // 2. Listener (I/O object)
    asio::ip::tcp::acceptor acceptor(
        ctx,
        asio::ip::tcp::endpoint(asio::ip::make_address("0.0.0.0"), 12345));

    // 3. Accept loop
    std::function<void()> accept_loop;
    accept_loop = [&]{
        auto socket = std::make_shared<asio::ip::tcp::socket>(ctx);
        acceptor.async_accept(*socket,
            [&, socket](const asio::error_code& ec){
                if (!ec) {
                    // 4. Echo back whatever is read
                    std::array<char, 1024> data;
                    socket->async_read_some(asio::buffer(data),
                        [socket, data](const asio::error_code& ec, std::size_t n) mutable {
                            if (!ec) {
                                asio::async_write(*socket,
                                    asio::buffer(data, n), [](auto...){});
                            }
                        });
                }
                accept_loop();   // keep accepting
            });
    };
    accept_loop();

    // 5. Run the event loop – drives all I/O objects
    ctx.run();
}

```

### Timer with Thread Pool Executor

This example shows how I/O objects can use executors from sources other than `io_context`, such as `thread_pool`.

```cpp
#include <asio.hpp>
#include <iostream>

int main()
{
    // 1. Create a thread-pool executor (I/O executor)
    asio::thread_pool pool(4);
    asio::any_io_executor exec = pool.get_executor();

    // 2. Timer I/O object bound to the executor
    asio::steady_timer timer(exec, std::chrono::seconds(2));

    // 3. Async wait
    timer.async_wait([&](const asio::error_code& ec){
        if (!ec) std::cout << "Timer fired on thread "
                          << std::this_thread::get_id() << "\n";
    });

    // 4. Run the pool – this drives the timer's executor
    pool.join();
}

```

### Composing Multiple I/O Objects

This coroutine example illustrates how multiple I/O objects (socket and timer) can be kept alive within a composed operation.

```cpp
#include <asio.hpp>
#include <iostream>

asio::awaitable<void> echo_with_timeout(asio::ip::tcp::socket sock,
                                        asio::steady_timer timer)
{
    char data[512];
    for (;;) {
        // Wait for either read completion or timeout
        std::size_t n = co_await asio::async_read(sock,
                         asio::buffer(data),
                         asio::as_tuple(asio::use_awaitable));
        co_await asio::async_write(sock,
                         asio::buffer(data, n),
                         asio::use_awaitable);
        timer.expires_after(std::chrono::seconds(10));
        co_await timer.async_wait(asio::use_awaitable); // keep connection alive
    }
}

```

The coroutine receives two I/O objects by value, ensuring both remain alive for the duration of the composed operation. This pattern demonstrates the flexibility of ASIO's executor model when managing multiple asynchronous resources.

## Summary

- **ASIO I/O objects** wrap asynchronous resources like sockets, timers, and acceptors, providing a unified interface for initiating operations in the `chriskohlhoff/asio` library.
- All I/O objects inherit from `basic_io_object` (defined in [`include/asio/basic_io_object.hpp`](https://github.com/chriskohlhoff/asio/blob/main/include/asio/basic_io_object.hpp)) and associate with an `any_io_executor` that controls handler dispatch.
- The `io_context` serves as the default executor and owns the low-level services that perform I/O, as implemented in [`include/asio/io_context.hpp`](https://github.com/chriskohlhoff/asio/blob/main/include/asio/io_context.hpp).
- Usage requires constructing objects with an executor, initiating asynchronous operations via `async_*` methods, and running the executor's event loop to process completions.
- Multiple I/O objects can be composed together using coroutines or `asio::compose`, with the executor ensuring proper work tracking and object lifetime management.

## Frequently Asked Questions

### What is the difference between an I/O object and an executor in ASIO?

An **I/O object** (such as `ip::tcp::socket` or `steady_timer`) represents the actual asynchronous resource and provides methods to initiate operations like `async_read` or `async_wait`. An **executor** (such as `any_io_executor` or the `io_context` executor) is responsible for scheduling and dispatching the completion handlers associated with those operations. While the I/O object initiates the work, the executor determines where and when the callback executes.

### Can I use ASIO I/O objects without an io_context?

Yes, you can construct I/O objects with any valid executor, such as those from `asio::thread_pool` or custom strand executors. However, the `io_context` remains the most common choice because it provides the core I/O services that actually perform the asynchronous operations. If you use a different executor, you must still ensure that an `io_context` or similar service is running somewhere to process the I/O, or use platform-specific native handles that don't require ASIO's service layer.

### How does ASIO ensure I/O objects remain valid during asynchronous operations?

ASIO uses **work tracking** through the executor associated with the I/O object. When an asynchronous operation is initiated, the service increments the work count for the executor. As shown in [`include/asio/compose.hpp`](https://github.com/chriskohlhoff/asio/blob/main/include/asio/compose.hpp), composition functions can accept multiple I/O objects to keep them alive until all operations complete. Additionally, using `std::shared_ptr` or capturing I/O objects in completion handlers (as demonstrated in the TCP echo server example) ensures they remain valid until the callbacks finish executing.

### Why is basic_io_object deprecated but still used in the codebase?

The `basic_io_object` class in [`include/asio/basic_io_object.hpp`](https://github.com/chriskohlhoff/asio/blob/main/include/asio/basic_io_object.hpp) is marked as deprecated because ASIO is transitioning toward a more executor-centric model where I/O objects directly store their implementation and executor references. However, the class remains the base for current I/O objects to maintain backward compatibility and provide a migration path. New code should be aware of this deprecation but can rely on the current inheritance structure until the library fully removes the deprecated base class in a future major version.