{"id":47294,"date":"2025-04-02T15:34:04","date_gmt":"2025-04-02T19:34:04","guid":{"rendered":"https:\/\/mjtsai.com\/blog\/?p=47294"},"modified":"2025-07-14T13:46:11","modified_gmt":"2025-07-14T17:46:11","slug":"swift-testing-return-errors-from-expectthrows","status":"publish","type":"post","link":"https:\/\/mjtsai.com\/blog\/2025\/04\/02\/swift-testing-return-errors-from-expectthrows\/","title":{"rendered":"Swift Testing: Return Errors From #expect(throws:)"},"content":{"rendered":"<p><a href=\"https:\/\/mjtsai.com\/blog\/2025\/04\/01\/swift-testing-test-scoping-traits\/\">Also<\/a> new in <a href=\"https:\/\/mjtsai.com\/blog\/2025\/03\/03\/swift-6-1\/\">Swift 6.1<\/a>, <a href=\"https:\/\/github.com\/swiftlang\/swift-evolution\/blob\/main\/proposals\/testing\/0006-return-errors-from-expect-throws.md\">ST-0006<\/a>:<\/p>\n<blockquote cite=\"https:\/\/github.com\/swiftlang\/swift-evolution\/blob\/main\/proposals\/testing\/0006-return-errors-from-expect-throws.md\"><p>We offer three variants of <code>#expect(throws:)<\/code>:<\/p><ul><li>One that takes an error type, and matches any error of the same type;<\/li><li>One that takes an error <em>instance<\/em> (conforming to <code>Equatable<\/code>) and matches any\nerror that compares equal to it; and<\/li><li>One that takes a trailing closure and allows test authors to write arbitrary\nvalidation logic.<\/li><\/ul><p>The third overload has proven to be somewhat problematic. First, it yields the\nerror to its closure as an instance of <code>any Error<\/code>, which typically forces the\ndeveloper to cast it before doing any useful comparisons. Second, the test\nauthor must return <code>true<\/code> to indicate the error matched and <code>false<\/code> to indicate\nit didn&rsquo;t, which can be both logically confusing and difficult to express\nconcisely[&#8230;]<\/p><\/blockquote>\n<p>Note that, with Swift 6.0, only the third variant actually lets you access the thrown error. So that&rsquo;s what I always used, but I found it awkward. I&rsquo;d been using a helper method to make it a little better:<\/p>\n\n<pre>\nfunc expect&#x3C;T, E&#x3E;(sourceLocation: SourceLocation = #_sourceLocation,\n                  _ performing: () throws -&#x3E; T,\n                  throws errorChecker: (E) -&#x3E; Void) {\n    \/\/ @SwiftIssue: Must write this separately or it won't type-check.\n    func errorMatcher(_ e: any Error) throws -&#x3E; Bool {\n        let error = try #require(e as? E, sourceLocation: sourceLocation)\n        errorChecker(error)\n        return true\n    }\n    #expect(sourceLocation: sourceLocation, \n            performing: performing, \n            throws: errorMatcher)\n}\n<\/pre>\n\n<p>This would do the cast, but I think it was still not great:<\/p>\n\n<pre>\nexpect {\n    try \/\/ something\n} throws: { (error: MJTError) in\n    \/\/ check `error`\n}\n<\/pre>\n<p>With Swift 6.1, the first two variants return the matched error, so I can just write:<\/p>\n<pre>\nlet error = try #require(throws: MJTError.self) {\n    try \/\/ something\n}\n\/\/ check `error`\n<\/pre>\n<p>Much better! Note that I&rsquo;ve switched from <code>#expect<\/code> to <code>#require<\/code> because it doesn&rsquo;t really work to do:<\/p>\n<pre>if let error = #expect(throws: MJTError.self) \/\/ ...<\/pre>\n\n<p>The <code>if<\/code> doesn&rsquo;t interact well with the trailing closure syntax, so I think I would have to write it like:<\/p>\n<pre>\nlet error = #expect(throws: MJTError.self) {\n    try \/\/ something\n}\nif let error {\n    \/\/ check `error`\n}\n<\/pre>\n<p>if I really wanted to continue after the error didn&rsquo;t match.<\/p>\n\n<p>Note that if you don&rsquo;t care what the error is but do want to look at it, you can pass <code>(any Error).self<\/code> as the error type.<\/p>\n\n<p>I was also wondering, now that we have <a href=\"https:\/\/mjtsai.com\/blog\/2023\/11\/28\/swift-proposal-typed-throws\/\">typed <code>throws<\/code><\/a>, why doesn&rsquo;t it check at compile time that the test code throws the right type of error? The <a href=\"https:\/\/github.com\/swiftlang\/swift-evolution\/blob\/main\/proposals\/testing\/0006-return-errors-from-expect-throws.md\">answer<\/a>:<\/p>\n<blockquote cite=\"https:\/\/github.com\/swiftlang\/swift-evolution\/blob\/main\/proposals\/testing\/0006-return-errors-from-expect-throws.md\">\n<p>If we adopted typed throws in the signatures of these macros, it would force adoption of typed throws in the code under test even when it may not be appropriate. For example, if we adopted typed throws, the following code would not compile[&#8230;]<\/p>\n<\/blockquote>\n\n<p>Previously:<\/p>\n<ul>\n<li><a href=\"https:\/\/mjtsai.com\/blog\/2025\/04\/01\/xcode-16-3\/\">Xcode 16.3<\/a><\/li>\n<li><a href=\"https:\/\/mjtsai.com\/blog\/2025\/04\/01\/swift-testing-test-scoping-traits\/\">Swift Testing: Test Scoping Traits<\/a><\/li>\n<li><a href=\"https:\/\/mjtsai.com\/blog\/2025\/03\/03\/swift-6-1\/\">Swift 6.1<\/a><\/li>\n<li><a href=\"https:\/\/mjtsai.com\/blog\/2024\/12\/17\/issues-adopting-swift-testing\/\">Issues Adopting Swift Testing<\/a><\/li>\n<li><a href=\"https:\/\/mjtsai.com\/blog\/2023\/11\/28\/swift-proposal-typed-throws\/\">Swift Proposal: Typed Throws<\/a><\/li>\n<\/ul>","protected":false},"excerpt":{"rendered":"<p>Also new in Swift 6.1, ST-0006: We offer three variants of #expect(throws:):One that takes an error type, and matches any error of the same type;One that takes an error instance (conforming to Equatable) and matches any error that compares equal to it; andOne that takes a trailing closure and allows test authors to write arbitrary [&hellip;]<\/p>\n","protected":false},"author":1,"featured_media":0,"comment_status":"open","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":{"apple_news_api_created_at":"2025-04-02T19:34:07Z","apple_news_api_id":"04d24d34-9af3-41e0-9c57-36fc23dacc60","apple_news_api_modified_at":"2025-04-02T19:34:07Z","apple_news_api_revision":"AAAAAAAAAAD\/\/\/\/\/\/\/\/\/\/w==","apple_news_api_share_url":"https:\/\/apple.news\/ABNJNNJrzQeCcVzb8I9rMYA","apple_news_coverimage":0,"apple_news_coverimage_caption":"","apple_news_is_hidden":false,"apple_news_is_paid":false,"apple_news_is_preview":false,"apple_news_is_sponsored":false,"apple_news_maturity_rating":"","apple_news_metadata":"\"\"","apple_news_pullquote":"","apple_news_pullquote_position":"","apple_news_slug":"","apple_news_sections":"\"\"","apple_news_suppress_video_url":false,"apple_news_use_image_component":false,"footnotes":""},"categories":[4],"tags":[46,71,901,2796,268],"class_list":["post-47294","post","type-post","status-publish","format-standard","hentry","category-programming-category","tag-languagedesign","tag-programming","tag-swift-programming-language","tag-swift-testing","tag-testing"],"apple_news_notices":[],"_links":{"self":[{"href":"https:\/\/mjtsai.com\/blog\/wp-json\/wp\/v2\/posts\/47294","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/mjtsai.com\/blog\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/mjtsai.com\/blog\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/mjtsai.com\/blog\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/mjtsai.com\/blog\/wp-json\/wp\/v2\/comments?post=47294"}],"version-history":[{"count":1,"href":"https:\/\/mjtsai.com\/blog\/wp-json\/wp\/v2\/posts\/47294\/revisions"}],"predecessor-version":[{"id":47295,"href":"https:\/\/mjtsai.com\/blog\/wp-json\/wp\/v2\/posts\/47294\/revisions\/47295"}],"wp:attachment":[{"href":"https:\/\/mjtsai.com\/blog\/wp-json\/wp\/v2\/media?parent=47294"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/mjtsai.com\/blog\/wp-json\/wp\/v2\/categories?post=47294"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/mjtsai.com\/blog\/wp-json\/wp\/v2\/tags?post=47294"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}