ELEMENT_TYPE_OF_SET_VIOLATES_HASHABLE_REQUIREMENTS

Is this possible while inserting a String into Set<String>

Crashed: com.apple.root.user-initiated-qos.cooperative
0  libswiftCore.dylib             0xf4c0 _assertionFailure(_:_:flags:) + 136
1  libswiftCore.dylib             0x17f484 ELEMENT_TYPE_OF_SET_VIOLATES_HASHABLE_REQUIREMENTS(_:) + 3792
2  MyEatApp                       0x44f6e8 specialized _NativeSet.insertNew(_:at:isUnique:) + 4333926120 (<compiler-generated>:4333926120)
3  MyEatApp                       0x44eaec specialized Set._Variant.insert(_:) + 4333923052 (<compiler-generated>:4333923052)
4  MyEatApp                       0x479f7c HomeViewModel.hanldeAnnouncementCard(from:) + 293 (HomeViewModel+PersonalizedOffer.swift:293)
5  libswift_Concurrency.dylib     0x5c134 swift::runJobInEstablishedExecutorContext(swift::Job*) + 292
6  libswift_Concurrency.dylib     0x5d5c8 swift_job_runImpl(swift::Job*, swift::SerialExecutorRef) + 156
7  libdispatch.dylib              0x13db0 _dispatch_root_queue_drain + 364
8  libdispatch.dylib              0x1454c _dispatch_worker_thread2 + 156
9  libsystem_pthread.dylib        0x9d0 _pthread_wqthread + 232
10 libsystem_pthread.dylib        0xaac start_wqthread + 8
Answered by DTS Engineer in 854736022

Thanks for the crash report.

That crash report shows two threads running inside HomeViewModel.hanldeAnnouncementCard(…), namely frame 5 on thread 22 and frame 13 on thread 24. Is there any chance they’re operating on the same instance of that HomeViewModel type?

If so, that’s problematic. hanldeAnnouncementCard(…) has no locking, so having two threads running in the same instance is a data race. And a data race could easily cause you to hit that assert within Set.insert(…).

Share and Enjoy

Quinn “The Eskimo!” @ Developer Technical Support @ Apple
let myEmail = "eskimo" + "1" + "@" + "apple.com"

Is this possible while inserting a String into Set<String>

I’m not 100% sure. Historically it might’ve been possible to get into this situation if you had a custom subclass of NSString, but I tried that here in my office today and I’m not able to trigger it any more [1].

The most common source of errors like this is folks not maintaining the Hashable invariant. I explain that in detail below. Are you sure that frame 4 is working with Set<String> rather than some custom type?

Share and Enjoy

Quinn “The Eskimo!” @ Developer Technical Support @ Apple
let myEmail = "eskimo" + "1" + "@" + "apple.com"

[1] For custom NSString subclasses, the compiler seems to eagerly bridge the contents over to a native String.


One of the fundamental requirements of Hashable is that, if you values are equal, they must have the same hash [1]. This is what allows containers like Set and Dictionary to use hashing to speed up things up:

  • They put each value in a bucket based on its hash.
  • To search for a value, they hash the value to find the right bucket and then just search that bucket.

If your implementation of Hashable doesn’t meet this requirement then you’ll see a variety of weird behaviours. For example, consider this code:

import Foundation

struct Person {
    var id: String
}

extension Person: Equatable {
}

extension Person: Hashable {
}

This code works as expected, relying on the compiler to synthesise the implementation of Equatable and Hashable. Now imagine that you want to ignore case when checking a person’s ID. So you add your own implementation of Equatable:

extension Person: Equatable {
    static func ==(_ lhs: Person, _ rhs: Person) -> Bool {
        lhs.id.caseInsensitiveCompare(rhs.id) == .orderedSame
    }
}

This seems to work; the following code never traps:

let u = UUID().uuidString
let p1 = Person(id: u)
let p2 = Person(id: u.lowercased())
assert(p1 == p2)

However, it’s not correct, because it’s possible for two equal values to have different hashes. This code will likely print hash mismatch:

if p1.hashValue != p2.hashValue { print("hash mismatch") }

Note I say “likely” because in the Swift standard library hashing include some degree of randomness to prevent collision attacks.

You’ll also see this bug cause problems with Set. For example, this code will sometimes print count mismatch:

var s = Set<Person>()
s.insert(p1)
s.insert(p2)
if s.count != 1 { print("count mismatch") }

And if you run a full test in a loop, it’ll eventually trap. For example this code:

func test() {
    let u = UUID().uuidString
    let p1 = Person(id: u)
    let p2 = Person(id: u.lowercased())
    assert(p1 == p2)
    if p1.hashValue != p2.hashValue { print("hash mismatch") }
    var s = Set<Person>()
    s.insert(p1)
    s.insert(p2)
    if s.count != 1 { print("count mismatch") }
}

for _ in 1...10_000 {
    test()
}

printed this:

hash mismatch
count mismatch
hash mismatch
hash mismatch
hash mismatch
hash mismatch
hash mismatch
hash mismatch
count mismatch
hash mismatch
count mismatch
hash mismatch
hash mismatch
Fatal error: Duplicate elements of type 'Person' were found in a Set.
This usually means either that the type violates Hashable's requirements, or
that members of such a set were mutated after insertion.

and then trapped with this backtrace:

(lldb) bt
…
    frame #0: … libswiftCore.dylib`_swift_runtime_on_report
    frame #1: … libswiftCore.dylib`_swift_stdlib_reportFatalError + 176
    frame #2: … libswiftCore.dylib`closure #1 (Swift.UnsafeBufferPointer<Swift.UInt8>) -> () in Swift._assertionFailure(_: Swift.StaticString, _: Swift.String, flags: Swift.UInt32) -> Swift.Never + 140
    frame #3: … libswiftCore.dylib`Swift._assertionFailure(_: Swift.StaticString, _: Swift.String, flags: Swift.UInt32) -> Swift.Never + 140
    frame #4: … libswiftCore.dylib`Swift.ELEMENT_TYPE_OF_SET_VIOLATES_HASHABLE_REQUIREMENTS(Any.Type) -> Swift.Never + 4896
    frame #5: … libswiftCore.dylib`Swift._NativeSet.insertNew(_: __owned τ_0_0, at: Swift._HashTable.Bucket, isUnique: Swift.Bool) -> () + 616
    frame #6: … libswiftCore.dylib`Swift.Set._Variant.insert(__owned τ_0_0) -> (inserted: Swift.Bool, memberAfterInsert: τ_0_0) + 1024
  * frame #7: … test`test() at main.swift:24:7
    frame #8: … test`main at main.swift:29:5
    frame #9: … dyld`start + 6076

In general, the fix for this is obvious: Always update your Equatable and Hashable conformances in unison to maintain this invariant. However, to offer specific advice about your code I’d need more details about how it’s implementing Hashable.

[1] Note that the reverse is not true.

Here is the code in question to provide more context

class HomeViewModel {

  var pOffers = [Offer]()
  var forceFetchOffers: Bool = false
  @MainActor var offersTable: [String: Offer] = [:]

  var handledAnnouncementIds: Set<String> = []
    
  func fetchOffersAndUpdateCards() {
      delegate?.setLoadingState(true)

      Task {
          await fetchCachedOffersIfNeeded()
          await refreshCardsFromCachedOffers()
      }
  }

  func fetchCachedOffersIfNeeded() async {
      guard shouldFetchOffers() else { return }
      pOffers = await personalization.getOffers()
      forceFetchOffers = false
  }

  func refreshCardsFromCachedOffers() async {
      let mappedCardsData = await mappedCardData(from: pOffers)
      await refreshOfferCards(with: mappedCardsData)
  }
    
  func mapToOffersCardData(offer: Offer) async -> OfferCardData? {
      switch offer.offerType {
      case .abc:
          return await hanldeAnnouncementCard(from: offer)
      case .xyz:
          return await addItemCardData(from: offer)
      default:
          return nil
      }
  }
  
  @MainActor
  func mappedCardData(from offers: [Offer]) async -> [OfferCardData] {
    offersTable.removeAll()
    handledAnnouncementIds = []

    var index: Int = 0
    return await offers
      .asyncCompactMap {
          guard var cardData = await self.mapToOffersCardData(offer: $0) else { return nil }
          cardData.cardIndex = index
          index += 1
          offersTable[cardData.id] = $0
          return cardData
      }
  }
  
  func hanldeAnnouncementCard(from offer: Offer) async -> OfferCardData? {
    if handledAnnouncementIds.contains(offer.itemID) {
        return nil
    }
    handledAnnouncementIds.insert(offer.itemID)
    return await announcementCardData(from: offer)
  }
    
  func announcementCardData(from offer: Offer) async -> OfferCardData? {
      guard let announcement = await fetchAnnouncement(for: offer.itemID) else { return nil }

      return ACardData(
        ......
        ......
      }
  }
  
  func showDefaultOfferCards() {
    Task {
      let offers = getDefaultOfferTypes()
      .map {
          Offer(
              offerType: $0,
              offerName: Constants.defaultCardOfferName
          )
      }
      
      let defaultCardsData = await mappedCardData(from: offers)
      await refreshDefaultOfferCards(with: defaultCardsData)
    }
  }      
}

public extension Sequence {
  func asyncCompactMap<T>(_ transform: (Element) async -> T?) async -> [T] {
    var values: [T] = []
    for element in self {
      guard let transformedElement = await transform(element) else { continue }
      values.append(transformedElement)
    }
    return values
  }
}

struct Offer {
  let itemID: String
  let offerType: OfferType
  let offerName: String
  
  init(
      offerType: OfferType,
      offerName: String = "",
      itemID: String = "",
  ) {
      self.offerType = offerType
      self.offerName = offerName
      self.itemID = itemID
  }
}

Thanks for the additional context.

Consider the backtrace in your original post:

3  MyEatApp … specialized Set._Variant.insert(_:) + 4333923052 (<compiler-generated>:4333923052)
4  MyEatApp … HomeViewModel.hanldeAnnouncementCard(from:) + 293 (HomeViewModel+PersonalizedOffer.swift:293)

That makes it clear that you’re failing on this line:

handledAnnouncementIds.insert(offer.itemID)

handledAnnouncementIds is a Set<String>, so it’s really hard to see how this would be because of a hashing problem.

Can you reproduce this in your office? Or are you debugging this based on crash reports coming in from the field?

Regardless, please post a full Apple crash report for this. See Posting a Crash Report for advice on how to do that.

Share and Enjoy

Quinn “The Eskimo!” @ Developer Technical Support @ Apple
let myEmail = "eskimo" + "1" + "@" + "apple.com"

Accepted Answer

Thanks for the crash report.

That crash report shows two threads running inside HomeViewModel.hanldeAnnouncementCard(…), namely frame 5 on thread 22 and frame 13 on thread 24. Is there any chance they’re operating on the same instance of that HomeViewModel type?

If so, that’s problematic. hanldeAnnouncementCard(…) has no locking, so having two threads running in the same instance is a data race. And a data race could easily cause you to hit that assert within Set.insert(…).

Share and Enjoy

Quinn “The Eskimo!” @ Developer Technical Support @ Apple
let myEmail = "eskimo" + "1" + "@" + "apple.com"

Thank you for your help with this. We were able to locate the code that caused the function to be executed from multiple threads.

ELEMENT_TYPE_OF_SET_VIOLATES_HASHABLE_REQUIREMENTS
 
 
Q