instructure/canvas-lms · error · GraphQL::ExecutionError

not found

Error message

not found

What it means

This is a deliberately translated error inside the updateDiscussionTopicParticipant GraphQL mutation. The resolver loads the DiscussionTopic by id, and any ActiveRecord::RecordNotFound (or an id belonging to a non-discussion object, since DiscussionTopic.find uses the ID type) is rescued and re-raised as a GraphQL::ExecutionError with the message "not found". GraphQL clients see it in the errors array rather than as an unhandled exception.

Solutions

  1. Verify the discussion_topic_id exists and is not deleted: DiscussionTopic.unscoped.find_by(id: ...).
  2. Ensure the client sends the correctly encoded Relay/legacy id expected by the mutation's ID prepare function.
  3. In multi-shard environments, use the shard-prefixed id so the record is found on the correct shard.
  4. Check the acting user has :read permission on the topic, since the same flow can 404/fail when access is lost.
  5. Handle the errors array in the GraphQL client and prompt the user to refresh/reselect the discussion.

Example fix

// before
updateDiscussionTopicParticipant(input: { discussionTopicId: "17", ... })
// after
// resolve the id through a fresh query first
topic = DiscussionTopic.unscoped.find_by(id: id)
raise "topic missing" unless topic
updateDiscussionTopicParticipant(input: { discussionTopicId: topic.relay_id, ... })
Defensive patterns

Strategy: try-catch

Validate before calling

// before calling the mutation
const topic = await fetchDiscussionTopic(id)
if (!topic) throw new Error(`discussion topic ${id} does not exist`)

Type guard

const isTopic = (t) => !!t && typeof t.id === 'string' && t.__typename === 'DiscussionTopic'

Try / catch

try {
  await updateDiscussionTopicParticipant(input)
} catch (e) {
  if (e.graphQLErrors?.some(g => g.message === 'not found')) {
    refreshDiscussionList() // topic likely deleted or id invalid
  } else { throw e }
}

Prevention

When it happens

Trigger: Calling mutation updateDiscussionTopicParticipant with a discussion_topic_id that does not exist, is soft-deleted, belongs to a different shard, or is a non-topic object whose ID collides in the Relay ID decoding.

Common situations: Clients caching a topic id after the topic was deleted; passing a legacy numeric id instead of the expected base64/Relay id (or vice versa depending on the prepare function); multi-shard setups where the id is not shard-prefixed so Switchman looks it up on the wrong shard.

Understand the failure class

Background: Record Not Found Errors: "not found", RecordNotFound, and "was not found" — what they mean and how to fix them — this error's family across 28 libraries.

Related errors


AI-assisted analysis of instructure/canvas-lms@1c9f0bb801 (2026-09-15). Data as JSON: /api/errors/0cfe6807247f5e45. Report an issue: GitHub.

Appendix: source

Thrown at app/graphql/mutations/update_discussion_topic_participant.rb:40

  graphql_name "UpdateDiscussionTopicParticipant"

  argument :discussion_topic_id, ID, required: true, prepare: GraphQLHelpers.relay_or_legacy_id_prepare_func("DiscussionEntry")
  argument :expanded, Boolean, required: false
  argument :has_unread_pinned_entry, Boolean, required: false
  argument :preferred_language, Types::PreferredLanguageType, required: false
  argument :show_pinned_entries, Boolean, required: false
  argument :sort_order, Types::DiscussionSortOrderType, required: false
  argument :summary_enabled, Boolean, required: false
  field :discussion_topic, Types::DiscussionType, null: false

  def resolve(input:)
    discussion_topic = DiscussionTopic.find(input[:discussion_topic_id])
    raise GraphQL::ExecutionError, "insufficient permission" unless discussion_topic.grants_right?(current_user, session, :read)

    discussion_topic.update_or_create_participant(current_user:, **input)
    { discussion_topic: }
  rescue ActiveRecord::RecordNotFound
    raise GraphQL::ExecutionError, "not found"
  end
end

View on GitHub (pinned to 1c9f0bb801)