# Web Serial API

> navigator.serial.requestPort() and SerialPort.open() connect a page to a serial device: options, every exception, and runnable read and write examples.

The Web Serial API lets a page ask the user for one serial device through `navigator.serial.requestPort()`, then exchange bytes with it over the returned `SerialPort`'s `readable` and `writable` streams. It reaches USB-to-serial adapters, built-in UART ports, and (from Chrome 117) Bluetooth RFCOMM services. The specification is published by the WHATWG; the older WICG URL redirects to it.

Chrome 89 and Edge 89 ship it on desktop and Firefox 151 followed; on Android, Chrome 148 exposes USB and Bluetooth ports while Chrome 138 to 147 and Samsung Internet 30 list Bluetooth RFCOMM ports only (BCD `api.Serial`). Safari, Firefox for Android, and Android WebView ([crbug.com/40740509](https://crbug.com/40740509)) have no implementation.

## Syntax

```js
navigator.serial.requestPort()
navigator.serial.requestPort(options)
navigator.serial.getPorts()

port.open(options)
port.close()
port.forget()
port.getInfo()
port.getSignals()
port.setSignals(signals)
```

`requestPort()` resolves with one `SerialPort` chosen in the browser's picker; `getPorts()` resolves with the array of ports this origin was already granted and that are attached. `open()`, `close()`, `forget()`, `getSignals()`, and `setSignals()` return promises. A `SerialPort` also exposes `readable` (a `ReadableStream` of `Uint8Array`, `null` while closed), `writable` (a `WritableStream`), and `connected` (a boolean, Chrome 130 and Firefox 151), and fires `connect` and `disconnect` events that bubble to `navigator.serial`. Every interface is `[SecureContext]` and `requestPort()` additionally needs transient user activation.

## Parameters

`requestPort(options)` takes a `SerialPortRequestOptions` dictionary; `open(options)` takes a `SerialOptions` dictionary in which only `baudRate` is required.

| Dictionary | Member | Type | Required | Description |
|---|---|---|---|---|
| `SerialPortRequestOptions` | `filters` | `sequence<SerialPortFilter>` | No | Restricts the picker to matching ports; without it every port is listed. |
| `SerialPortRequestOptions` | `allowedBluetoothServiceClassIds` | `sequence<BluetoothServiceUUID>` | No | Extra Bluetooth service class IDs to offer besides the standard Serial Port Profile (Chrome 117). |
| `SerialPortFilter` | `usbVendorId` | `unsigned short` | Only when `usbProductId` is set | USB vendor ID of the adapter. |
| `SerialPortFilter` | `usbProductId` | `unsigned short` | No | USB product ID; requires `usbVendorId`. |
| `SerialPortFilter` | `bluetoothServiceClassId` | `BluetoothServiceUUID` | No | Bluetooth service class; a filter that also sets a USB member is rejected with `TypeError`. |
| `SerialOptions` | `baudRate` | `unsigned long` | Yes | Bits per second; must be greater than 0. |
| `SerialOptions` | `dataBits` | `octet` | No, default `8` | 7 or 8. |
| `SerialOptions` | `stopBits` | `octet` | No, default `1` | 1 or 2. |
| `SerialOptions` | `parity` | `ParityType` | No, default `"none"` | `"none"`, `"even"`, or `"odd"`. |
| `SerialOptions` | `bufferSize` | `unsigned long` | No, default `255` | Size of the read and write buffers in bytes; must be greater than 0. |
| `SerialOptions` | `flowControl` | `FlowControlType` | No, default `"none"` | `"none"` or `"hardware"` (RTS/CTS). |

`setSignals(signals)` takes a `SerialOutputSignals` dictionary with the booleans `dataTerminalReady`, `requestToSend`, and `break`; at least one member must be present. `getSignals()` resolves with `SerialInputSignals` (`dataCarrierDetect`, `clearToSend`, `ringIndicator`, `dataSetReady`), and `getInfo()` returns `usbVendorId`, `usbProductId`, and `bluetoothServiceClassId` when the port exposes them.

## Exceptions

| Method | Exception | Condition |
|---|---|---|
| `requestPort()` | `SecurityError` | The document is not allowed to use the `serial` Permissions Policy feature, or the call has no transient activation. |
| `requestPort()` | `TypeError` | A filter combines `bluetoothServiceClassId` with a USB member, or sets `usbProductId` without `usbVendorId`. |
| `requestPort()` | `NotFoundError` | The user closed the picker without choosing a port. |
| `getPorts()` | `SecurityError` | The `serial` Permissions Policy feature is disallowed. |
| `open()` | `InvalidStateError` | The port state is not `"closed"` (already open, or an `open()` or `close()` is in flight). |
| `open()` | `TypeError` | `baudRate` is 0, `dataBits` is not 7 or 8, `stopBits` is not 1 or 2, `bufferSize` is 0 or larger than the implementation supports. |
| `open()` | `NetworkError` | The operating system failed to open the port. |
| `readable` (stream error) | `BufferOverrunError`, `BreakError`, `FramingError`, `ParityError` | The matching line condition was detected; the stream errors and must be re-acquired from `port.readable`. |
| `readable` / `writable` | `UnknownError` | An operating system error occurred while reading or writing. |
| `readable` / `writable` | `NetworkError` | The device was disconnected; `writable` also sets a fatal flag so the port must be closed. |
| `writable` | `TypeError` | A written chunk is not a `BufferSource`. |
| `setSignals()`, `getSignals()`, `close()` | `InvalidStateError` | The port is not `"opened"`. |
| `setSignals()` | `TypeError` | The signals dictionary has no member. |
| `setSignals()`, `getSignals()` | `NetworkError` | The operating system refused to set or read the control lines. |

:::observed
Chrome's strings for the common failures, all thrown from [`serial.cc`](https://chromium.googlesource.com/chromium/src/+/main/third_party/blink/renderer/modules/serial/serial.cc) and [`serial_port.cc`](https://chromium.googlesource.com/chromium/src/+/main/third_party/blink/renderer/modules/serial/serial_port.cc) (chromium.googlesource.com): calling `requestPort()` outside a click handler rejects with `SecurityError: Must be handling a user gesture to show a permission request.`; closing the picker rejects with `NotFoundError: No port selected by the user.`; a second `open()` on an open port rejects with `InvalidStateError: The port is already open.`; `open({ baudRate: 0 })` rejects with `TypeError: Requested baud rate must be greater than zero.`; and a port another program already holds rejects with `NetworkError: Failed to open serial port.`. Unplugging the adapter mid-read errors the stream with `NetworkError: The device has been lost.`.
:::

## Examples

Each example feature-detects `navigator.serial` and shows the branch that runs without it. Only `requestPort()` needs a user gesture; everything after it can run from ordinary code.

### Reusing a granted port before asking for a new one

`getPorts()` returns ports the user already approved, so a returning visitor should not see the picker again. When the API is missing (Safari, Firefox for Android, WebView) the function reports that and leaves the caller to offer a desktop link or a native companion app.

```js
async function connect(vendorId) {
  if (!('serial' in navigator)) {
    return { port: null, reason: 'Web Serial is not available in this browser.' };
  }

  const granted = await navigator.serial.getPorts();
  let port = granted.find((p) => p.getInfo().usbVendorId === vendorId);

  if (!port) {
    try {
      port = await navigator.serial.requestPort({ filters: [{ usbVendorId: vendorId }] });
    } catch (err) {
      if (err.name === 'NotFoundError') return { port: null, reason: 'No device chosen.' };
      throw err;
    }
  }

  await port.open({ baudRate: 115200 });
  return { port, reason: null };
}
```

The `filters` entry keeps unrelated ports out of the picker; omit it to list every port on the machine.

### Reading text lines until the device disconnects

Pipe `readable` through a `TextDecoderStream` and loop on the reader. A disconnect errors the stream with `NetworkError`; a parity or framing fault errors it with the specific name while the port stays open, so the loop re-acquires `port.readable` and continues.

```js
async function readLines(port, onLine) {
  while (port.readable) {
    const reader = port.readable.pipeThrough(new TextDecoderStream()).getReader();
    let buffer = '';
    try {
      for (;;) {
        const { value, done } = await reader.read();
        if (done) break;
        buffer += value;
        const lines = buffer.split('\n');
        buffer = lines.pop();
        lines.forEach(onLine);
      }
    } catch (err) {
      if (err.name === 'NetworkError') return; // device gone
      console.warn(`${err.name}: ${err.message}`); // parity, framing, break, overrun
    } finally {
      reader.releaseLock();
    }
  }
}
```

`releaseLock()` in `finally` matters: `port.close()` waits for the readable lock to be released and otherwise hangs.

### Sending a command and closing cleanly

Writing uses a `WritableStream` writer; `close()` requires both streams to be unlocked, so release the writer first.

```js
async function sendAndClose(port, command) {
  const writer = port.writable.getWriter();
  try {
    await writer.write(new TextEncoder().encode(`${command}\r\n`));
  } finally {
    writer.releaseLock();
  }
  await port.close();
}
```

After `close()` resolves, `port.readable` and `port.writable` are `null` until the next `open()`.

## See also

- [WebUSB API](/reference/capabilities/web-usb/), for devices without a serial profile
- [WebHID API](/reference/capabilities/web-hid/)
- [Web Bluetooth API](/reference/capabilities/web-bluetooth/), for GATT rather than RFCOMM devices
- [Web Serial API: requestPort() method](https://serial.spec.whatwg.org/#dom-serial-requestport) (whatwg.org)
- [Web Serial API: SerialOptions dictionary](https://serial.spec.whatwg.org/#dom-serialoptions) (whatwg.org)
- [Read from and write to a serial port](https://developer.chrome.com/docs/capabilities/serial) (developer.chrome.com)
- [Chromium bug 40740509: Web Serial in Android WebView](https://crbug.com/40740509) (crbug.com)