Skip to main content
fetchMembers() does not work against any current Cloak server. The opcode it waits on became direct-message-only server-side, so a server-scoped request gets no reply frame at all, not even an error code. The promise never resolves and never rejects: it hangs until the transport timeout. A try/catch catches nothing, and await blocks your handler for the whole wait.This is not a permission problem, a flake, or something a retry fixes. There is no workaround inside the SDK. The signature will survive the rebuild, so code written against it stays valid, but it cannot run today. fetchRoles() is unaffected and works normally.
The Client has two on-demand server reads. fetchRoles() returns the server’s roles, and it works. fetchMembers() returns the member roster, and it is currently out of service. Neither read caches, so each call goes to the server and gives you a point-in-time answer.

Fetch the roles

client.fetchRoles(serverId) returns a Promise<Role[]>. It reads on demand and does not cache.
Each entry describes one role in the server.
string
The role’s id.
string
The role’s name.
string | null
The role’s primary color, or null if none is set.
string | null
The role’s secondary color, or null if none is set.
number | null
The role’s position, or null if unknown. Lower is higher in the list.
number
How many members hold this role.

A !roles command

fetchRoles() is a server read, so guard the handler first. Message.serverId is string | null and is null for a direct message, which arrives on the same messageCreate event.
The reply uses msg.channel.send(), a plain send. msg.reply() would post a true reply with a quote header and fire a “replied to you” notification, which is more than a command answer needs.
msg.channel.send() is fire-and-forget. Its promise resolves when the frame leaves the process, not when the server accepts it. A server-side denial arrives later on the sendRejected event, so listen for that if you need to know a send was refused.

Counting members without fetchMembers()

Role.memberCount is the one live membership number a bot can read today. Summing it does not give you a server total, because a member holds at most one role and members with no role are counted nowhere, but it is enough for “how many people hold this role”.
For per-person information, use what the firehose already hands you. Every messageCreate carries msg.authorId and msg.authorName, so a bot can build its own map of the people it has actually seen.

The member roster read

client.fetchMembers(serverId) is declared as Promise<Member[]>. Do not call it against a live server. See the warning at the top of this page: the call hangs rather than failing, so a bot that reaches it stalls the handler that awaited it. The shape is documented here so code you write now still compiles once the read is rebuilt.
string
The member’s normalized id.
string
The member’s username.
number
An online-status code. 0 means offline.
Date | null
When the account was created, when known. This is the account creation time, not the time the member joined this server. null when unknown.
string | null
The member’s icon, or null if none is set.
number | null
The member’s subscription tier, or null if unknown.
boolean
true if this member is a bot.
MemberRole | null
The member’s assigned role, or null if they have none. A MemberRole is { id, name, color, secondColor, admin }, where admin is true when the role carries admin.
If you already ship a bot that calls fetchMembers(), the symptom is a handler that appears to do nothing and never logs an error. Remove the call or put it behind a flag you leave off. Wrapping it in Promise.race() with your own timer bounds the wait, but there is still no roster to read at the end of it.

There is no live roster stream

These reads hit the server every time, so treat a result as a snapshot rather than a live view. Call again when you need current data. Cloak has no continuous member-roster feed for a bot. The firehose carries messages, not membership. The one inbound membership signal is the systemMessage event with type: 'member_join', which fires when someone joins a server the bot is in.
SystemMessageEvent carries actorId, actorName, serverId, channelId, and an optional groupId resolved from the bot’s channel-group cache. Pass groupId through in SendOptions so the send does not have to guess the channel’s group. The voice roster events are a separate thing and are not a firehose either. They reach a bot only for the server it currently has selected or is in voice in, so they are not a general membership feed.

Read through the guild() handle

Both reads are bound on the guild() handle when you already have one for the server.
g.fetchMembers() binds the same out-of-service read as the flat method and hangs the same way. Only g.fetchRoles() works.
See The guild() handle for the rest of what the handle binds.

Handle failures

fetchRoles() rejects with a CloakActionError when the server refuses the read or errors while serving it. Wrap it and decide what to do when the roles are not available.
The server decides whether your bot may read this data and is the authority on the result. A refused read rejects rather than returning a partial list. fetchMembers() has no working failure path today. It does not reject, so the catch above would never run for it.

Next

Types

Every field on Role, Member, and MemberRole.

Known limitations

What else is out of service, and what is tracked.

The guild() handle

The other way to reach these reads.

Moderation

Act on a member once you have their id.