Capital Gazette

ens ethers.js

How ens ethers.js works: everything you need to know

June 16, 2026 By Greer Rivera

Introduction: ethers.js and the ENS ecosystem

The Ethereum Name Service (ENS) transforms human-readable names like alice.eth into machine-readable identifiers — wallet addresses, content hashes, text records, and more. Ethers.js, the leading JavaScript library for Ethereum interaction, provides first-class support for ENS through its Provider and EnsResolver APIs. This article walks you through every mechanism by which ethers.js resolves, manages, and extends ENS functionality, from name resolution to avatar setup.

Understanding how ethers.js implements ENS is essential for any dApp developer. It abstracts away the low-level contract calls to the ENS registry (0x00000000000C2E074eC69A0dFb2997BA6C7d2e1e on mainnet) and the associated resolver contracts. The library automatically detects ENS TLDs (e.g., .eth, .xyz, .luxe) and routes them through the appropriate resolver logic. Before diving into specific operations, a fundamental concept to grasp is the separation between name resolution (forward lookup) and address resolution (reverse lookup). Ethers.js handles both seamlessly.

Core ENS resolution with ethers.js

Ethers.js resolves ENS names through the resolveName method on any provider instance. The resolution pipeline follows the ENSIP-1 standard: it first normalizes the name using UTS-46, then queries the ENS registry for the resolver contract address, and finally calls addr(bytes32) on that resolver. Consider this code pattern:

import { ethers } from 'ethers';
const provider = new ethers.JsonRpcProvider('YOUR_RPC_URL');
const address = await provider.resolveName('vitalik.eth');
// '0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045'

The method returns a lowercase hex string or null if the name doesn't exist. For reverse resolution — converting an address back to an ENS name — ethers.js provides lookupAddress:

const name = await provider.lookupAddress('0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045');
// 'vitalik.eth'

Under the hood, this calls the reverse registrar contract at 0x9062c0a6Dbd6108336Bcbe4593a3D1cE055120F5 and performs a forward resolution to verify match. Note that lookupAddress may return null if the address has no reverse record or the forward verification fails. For batch operations — for example, resolving 100 names in a single block — use resolveNames which returns an array of addresses in the same order as input names. Performance-wise, each resolution costs one provider call, so caching resolved addresses locally (e.g., in a Map) is recommended for UI rendering.

Working with ENS text records and content hashes

Beyond basic address resolution, ENS stores arbitrary key-value text records and content hashes for IPFS, Swarm, or other decentralized storage. Ethers.js exposes these through the EnsResolver class, which you obtain via provider.getResolver(name). The resolver object provides typed methods:

  • getText(key) — retrieves text records by key (e.g., 'email', 'url', 'description', 'com.twitter')
  • getAvatar() — fetches the ENS avatar URI, which may be an IPFS, HTTPS, or data URL
  • getContentHash() — returns the encoded content hash (as hex string) for IPNS, IPFS, or Swarm

The most useful pattern for developers is avatar resolution. The ENS avatar spec allows a name to point to an image via the 'avatar' text record. Here's how ethers.js retrieves it:

const resolver = await provider.getResolver('nick.eth');
const avatarUri = await resolver.getAvatar();
// 'https://ipfs.io/ipfs/Qm...'

The getAvatar() method automatically decodes IPFS URIs (ipfs://) into HTTP gateways and validates the image format. If you're building a profile application, you can combine getAvatar() with text records like 'description' and 'com.github' to create a rich user card. For a complete walkthrough on configuring these records, refer to an ens avatar setup tutorial that covers setting the avatar text record through the ENS manager app.

Content hashes deserve special attention for decentralized apps. When a dApp stores its content on IPFS, it registers the content hash under the ENS name. Ethers.js parses the raw 32-byte hash into a structured object with protocolType and hash properties. For ipns:// links, the protocol is 'ipns' and the hash is a base36-encoded CID. For swarm://, it's 'bzz' with a 32-byte Swarm hash. The getContentHash() method handles all three variants transparently.

Managing ENS records with a signer

Ethers.js also supports writing ENS records — but only through a Signer (not a read-only provider). Writing records requires paying gas and possessing the private key that controls the ENS name. The process involves three steps: acquiring the resolver instance from a signer-connected provider, encoding the method call, and sending the transaction. For example, to set a text record:

const signer = new ethers.Wallet('PRIVATE_KEY', provider);
const resolver = await provider.getResolver('my-name.eth');
// Set a text record
const resolverContract = new ethers.Contract(
    resolver.address,
    ['function setText(bytes32 node, string key, string value)'],
    signer
);
const tx = await resolverContract.setText(
    ethers.namehash('my-name.eth'),
    'com.twitter',
    '@myhandle'
);
await tx.wait();

This pattern works for all writable resolver methods: setAddr (to change the primary address), setContentHash (for dApp hosting), and setText (for any arbitrary record). Note that the namehash function computes the ENS node from the name — ethers.js provides this as ethers.namehash(name). The resolver contract address is obtained from the registry, but you must manually construct the contract interface as shown above. For production applications, consider using the ens-resolver package that provides typed wrappers.

A common real-world scenario is changing the wallet address associated with an ENS name during a wallet migration. With ethers.js, you call setAddr on the resolver, passing the new address as bytes. This effectively lets you Replace your crypto wallet address without losing the ENS name or paying transfer fees. The transaction updates the resolver's records, and after one confirmation, forward resolution returns the new address. The old address remains valid for reverse resolution only if you also update the reverse record via the reverse registrar.

Subdomain management and batch operations

ENS enables subdomain creation — for example, pay.yourapp.eth — where yourapp.eth controls subdomain allocation. Ethers.js interacts with the ENS registry's setSubnodeOwner method to mint subdomains. The typical flow uses a signer authorized as the parent name's controller:

const resolver = await provider.getResolver('yourapp.eth');
const registry = new ethers.Contract(
    '0x00000000000C2E074eC69A0dFb2997BA6C7d2e1e',
    ['function setSubnodeOwner(bytes32 node, bytes32 label, address owner)'],
    signer
);
const labelHash = ethers.id('pay');
const tx = await registry.setSubnodeOwner(
    ethers.namehash('yourapp.eth'),
    labelHash,
    newOwnerAddress
);
await tx.wait();
// Now pay.yourapp.eth exists, owned by newOwnerAddress

After creating the subdomain, you must separately set its resolver and records. The subdomain inherits no records from the parent by default — each record must be explicitly configured. For batch operations across many subdomains, consider using the Multicall contract to batch setText and setAddr calls in a single transaction. Ethers.js supports multicall through its Interface class, but ENS-specific batching requires manual ABI encoding.

Gas optimization is critical here. Each setSubnodeOwner costs approximately 45,000 gas, and each text record update costs ~35,000 gas. For 100 subdomains with 5 records each, you're looking at 4,000,000 gas — potentially $500+ at peak rates. Use a Multicall2 contract to batch updates into 5 transactions instead of 500. Ethers.js provides contract.populateTransaction to generate transaction data, which you can then pass to the multicall contract.

Error handling and edge cases

Reliable ENS integration requires handling at least four categories of errors. First, unregistered namesresolveName returns null, but the promise never rejects. Always check for falsy return values. Second, expired names — the ENS registry still resolves expired names until the grace period ends (90 days after expiry). After that, the name's records remain but the name can be re-registered by anyone. Third, malformed names — names with invalid characters throw encoding errors. Use ethers.isValidName(name) before resolution. Fourth, RPC errors — if the node doesn't support the eth_call with state override, fall back to manual contract instantiation.

Test your ENS integration against both mainnet and Sepolia testnet. Sepolia has a fully functional ENS deployment at the same registry address. For local development, deploy a mock registry or use the Hardhat ENS plugin. Remember that getResolver may return null for names that have no custom resolver set (defaulting to the public resolver). Always check resolver !== null before calling methods like getText.

Conclusion: building production-ready ENS features

Ethers.js provides the foundational primitives for ENS integration — name resolution, record management, and subdomain creation. The library's resolveName and lookupAddress cover 90% of use cases, while the EnsResolver class unlocks text records and avatars. For write operations, you must drop to raw contract interactions using the ethers.Contract pattern, but the underlying methods remain consistent with ENSIP standards. As you scale, invest in caching, batch operations, and comprehensive error handling to handle the 10% edge cases that break naive implementations. Start by resolving a few names, then extend to avatar display, then graduate to multi-record management. The ENS ecosystem rewards developers who invest in understanding its resolution pipeline — ethers.js makes that investment straightforward.

Reference: ens ethers.js tips and insights

Background & Citations

G
Greer Rivera

Reviews, without the noise