Async checked (raising) exceptions.md
1 For a seasoned Nim developer a lot of things I am writing here may be obvious, but for those in a continuous learning path, it may bring some consolation. 2 3 The [The Status Nim style guide](https://status-im.github.io/nim-style-guide) recommends [explicit error handling mechanisms](https://status-im.github.io/nim-style-guide/errors.result.html) and handling errors locally at each abstraction level, avoiding spurious abstraction leakage. This is in contrast to leaking exception types between layers and translating exceptions that causes high visual overhead, specially when hierarchy is used, often becoming not practical, loosing all advantages of using exceptions in the first place (read more [here](https://status-im.github.io/nim-style-guide/errors.exceptions.html)). 4 5 Handling error and working with exceptions is easier to grasp when not using asynchronous code. But when you start, there are some subtle traps you may be falling into. 6 7 This short note focuses on asynchronous code. It is not complete, but pragmatic, it has gaps, but provides directions if one wants to research further. 8 9 In our code we often use the following patterns: 10 1. using [nim-results](https://github.com/arnetheduck/nim-results) and [std/options](https://nim-lang.org/docs/options.html) to communicate the results and failures to the caller, 11 2. *async* operations are annotated with `{.async: (raises: [CancelledError]).}`. 12 13 Some interesting things are happening when you annotate a Nim `proc` with "async raises". 14 15 Let's looks at some examples. 16 17 Imagine you have the following type definitions: 18 19 ```nim 20 type 21 MyError = object of CatchableError 22 Handle1 = Future[void].Raising([CancelledError]) 23 Handle2 = Future[void].Raising([CancelledError, MyError]) 24 25 SomeType = object 26 name: string 27 handle1: Handle1 28 handle2: Handle2 29 ``` 30 31 `Handle1` and `Handle2` are *raising exceptions*. By using `Rasing` macro, passing the list of allowed exceptions coming out from the future as an argument, `Handle1` and `Handle2` are no longer well-known `Future[void]`, but rather a descendant of it: 32 33 ```nim 34 type 35 InternalRaisesFuture*[T, E] = ref object of Future[T] 36 ## Future with a tuple of possible exception types 37 ## eg InternalRaisesFuture[void, (ValueError, OSError)] 38 ## 39 ## This type gets injected by `async: (raises: ...)` and similar utilities 40 ## and should not be used manually as the internal exception representation 41 ## is subject to change in future chronos versions. 42 # TODO https://github.com/nim-lang/Nim/issues/23418 43 # TODO https://github.com/nim-lang/Nim/issues/23419 44 when E is void: 45 dummy: E 46 else: 47 dummy: array[0, E] 48 ``` 49 50 The comment is saying something important: if you annotate a `proc` with `async: (raises: ...)`, you are changing the type being returned by the `proc`. To see what does it mean, lets start with something easy. Let's write a constructor for `SomeType`: 51 52 ```nim 53 proc newSomeType(name: string): SomeType = 54 let t = SomeType( 55 name: name, 56 # both fail 57 handle1: newFuture[void](), 58 handle2: newFuture[void](), 59 ) 60 t 61 ``` 62 63 Well, this will not compile. `handle1` expects `InternalRaisesFuture[system.void, (CancelledError,)]`, but instead it gets `Future[system.void]`. Yes, we are trying to cast a more generic to a less generic type. This is because `newSomeType` is not annotated with `async: (raises: ...)` and therefore every time you use `newFuture` inside it, `newFuture` returns regular `Future[void]`. 64 65 So, the first time I encountered this problem I went into a rabbit hole of understanding how to create *raising* futures by solely relaying on `async: (raises: ...)` pragma. But there is actually a public (I guess) interface allowing us to create a raising future without relaying on `async: (raises: ...)` annotation: 66 67 ``` 68 ```nim 69 proc newSomeType(name: string): SomeType = 70 let t = SomeType( 71 name: name, 72 # both fail 73 handle1: Future[void].Raising([CancelledError]).init(), 74 handle2: Future[void].Raising([CancelledError, MyError]).init(), 75 ) 76 t 77 ``` 78 79 A bit verbose, but perfectly fine otherwise, and it works as expected: 80 81 ```nim 82 let someTypeInstance = newSomeType("test") 83 84 echo typeof(someTypeInstance.handle1) # outputs "Handle1" 85 echo typeof(someTypeInstance.handle2) # outputs "Handle2" 86 ``` 87 88 `init` has the following definition: 89 90 ```nim 91 template init*[T, E]( 92 F: type InternalRaisesFuture[T, E], fromProc: static[string] = ""): F = 93 ## Creates a new pending future. 94 ## 95 ## Specifying ``fromProc``, which is a string specifying the name of the proc 96 ## that this future belongs to, is a good habit as it helps with debugging. 97 when not hasException(type(E), "CancelledError"): 98 static: 99 raiseAssert "Manually created futures must either own cancellation schedule or raise CancelledError" 100 101 102 let res = F() 103 internalInitFutureBase(res, getSrcLocation(fromProc), FutureState.Pending, {}) 104 res 105 ``` 106 107 and is very similar to: 108 109 ```nim 110 proc newInternalRaisesFutureImpl[T, E]( 111 loc: ptr SrcLoc, flags: FutureFlags): InternalRaisesFuture[T, E] = 112 let fut = InternalRaisesFuture[T, E]() 113 internalInitFutureBase(fut, loc, FutureState.Pending, flags) 114 fut 115 ``` 116 117 thus, if we had exposed the internals, our example would be: 118 119 ```nim 120 proc newSomeType(name: string): SomeType = 121 let t = SomeType( 122 name: name, 123 handle1: newInternalRaisesFuture[void, (CancelledError,)](), 124 handle2: newInternalRaisesFuture[void, (CancelledError, MyError)](), 125 ) 126 t 127 ``` 128 129 130 It is still very educational to study the chronos source code to undertstand how does the `newFuture` know which type to return: `Future[T]` or `InternalRaisesFuture[T, E]` when a proc is annotated with `async` or `async: (raises: ...)`. 131 132 If you study `chronos/internal/asyncfutures.nim` you will see that `newFuture` is implemented with the following template: 133 134 ```nim 135 template newFuture*[T](fromProc: static[string] = "", 136 flags: static[FutureFlags] = {}): auto = 137 ## Creates a new future. 138 ## 139 ## Specifying ``fromProc``, which is a string specifying the name of the proc 140 ## that this future belongs to, is a good habit as it helps with debugging. 141 when declared(InternalRaisesFutureRaises): # injected by `asyncraises` 142 newInternalRaisesFutureImpl[T, InternalRaisesFutureRaises]( 143 getSrcLocation(fromProc), flags) 144 else: 145 newFutureImpl[T](getSrcLocation(fromProc), flags) 146 ``` 147 148 We see the the actual implementation depends on the existence of `InternalRaisesFutureRaises`. Let's see how it is being setup... 149 150 The `async` pragma is a macro defined in `chronos/internal/asyncmacro.nim`: 151 152 ```nim 153 macro async*(params, prc: untyped): untyped = 154 ## Macro which processes async procedures into the appropriate 155 ## iterators and yield statements. 156 if prc.kind == nnkStmtList: 157 result = newStmtList() 158 for oneProc in prc: 159 result.add asyncSingleProc(oneProc, params) 160 else: 161 result = asyncSingleProc(prc, params) 162 ``` 163 164 The `asyncSingleProc` is where a lot of things happen. This is where the errors *The raises pragma doesn't work on async procedures* or *Expected return type of 'Future' got ...* come from. The place where the return type is determined is interesting: 165 166 ```nim 167 let baseType = 168 if returnType.kind == nnkEmpty: 169 ident "void" 170 elif not ( 171 returnType.kind == nnkBracketExpr and 172 (eqIdent(returnType[0], "Future") or eqIdent(returnType[0], "InternalRaisesFuture"))): 173 error( 174 "Expected return type of 'Future' got '" & repr(returnType) & "'", prc) 175 return 176 else: 177 returnType[1] 178 ``` 179 180 An async proc can have two (explicit) return types: `Future[baseType]` or `InternalRaisesFuture[baseType]`. If no return type is specified for an async proc, the return base type is concluded to be `void`. Now the crucial part: the internal return type (we are still inside of `asyncSingleProc`): 181 182 ```nim 183 baseTypeIsVoid = baseType.eqIdent("void") 184 (raw, raises, handleException) = decodeParams(params) 185 internalFutureType = 186 if baseTypeIsVoid: 187 newNimNode(nnkBracketExpr, prc). 188 add(newIdentNode("Future")). 189 add(baseType) 190 else: 191 returnType 192 internalReturnType = if raises == nil: 193 internalFutureType 194 else: 195 nnkBracketExpr.newTree( 196 newIdentNode("InternalRaisesFuture"), 197 baseType, 198 raises 199 ) 200 ``` 201 202 To focus on the most important part, at the end we see that if `raises` attribute is present and set (`async: (raises: [])` means it does not raise, but the attribute is still present and detected), the `internalReturnType` will be set to: 203 204 ```nim 205 nnkBracketExpr.newTree( 206 newIdentNode("InternalRaisesFuture"), 207 baseType, 208 raises 209 ) 210 ``` 211 212 Thus, for `async: (raises: [CancelledError, ValueError])`, the return type will be `InternalRaisesFuture[baseType, (CancelledError, ValueError,)`. 213 214 If the `async` has `raw: true` param set, e.g. `async: (raw: true, raises: [CancelledError, ValueError])`, then `prc.body` gets prepended with the type definition we already recognize from `newFuture` above: `InternalRaisesFutureRaises` 215 216 ```nim 217 if raw: # raw async = body is left as-is 218 if raises != nil and prc.kind notin {nnkProcTy, nnkLambda} and not isEmpty(prc.body): 219 # Inject `raises` type marker that causes `newFuture` to return a raise- 220 # tracking future instead of an ordinary future: 221 # 222 # type InternalRaisesFutureRaises = `raisesTuple` 223 # `body` 224 prc.body = nnkStmtList.newTree( 225 nnkTypeSection.newTree( 226 nnkTypeDef.newTree( 227 nnkPragmaExpr.newTree( 228 ident"InternalRaisesFutureRaises", 229 nnkPragma.newTree(ident "used")), 230 newEmptyNode(), 231 raises, 232 ) 233 ), 234 prc.body 235 ) 236 ``` 237 238 For our example of `async: (raw: true, raises: [CancelledError, ValueError])`, this will be: 239 240 ```nim 241 type InternalRaisesFutureRaises {.used.} = (CancelledError, ValueError,) 242 ``` 243 244 This allows the `newFuture` template to recognize it has to use `InternalRaisesFuture` as the return type. 245 246 ### Experimenting with *Raising Futures* 247 248 With the `Future[...].Raising(...).init()` construct we can quite elegantly create new raising futures in regular proc not annotated with `async: (raises: ...)`. But to get more intuition, let's play a bit with creating our own version of `Future[...].Raising(...).init()` that will be built on top of `async: (raises: ...)` pragma. 249 250 > [!info] 251 > This is just an exercise. It reveals some interesting details about how `async` is implemented. I also used it to learn some basics about using macros and how they can help where generics have limitations. 252 253 Let's start with creating a proc that returns type `Handle1`? 254 255 Recall that `Handle1` is defined as follows: 256 257 ```nim 258 type 259 Handle1 = Future[void].Raising([CancelledError]) 260 ``` 261 262 ```nim 263 proc newHandle1(): Handle1 {.async: (raw: true, [CancelledError]).} = 264 newFuture[void]() 265 266 proc newSomeType(name: string): SomeType = 267 let t = SomeType( 268 name: name, 269 handle1: newHandle1(), 270 handle2: Future[void].Raising([CancelledError, MyError]).init(), 271 ) 272 t 273 ``` 274 275 That would be nice an concise, yet, you remember now the "Expected return type of 'Future' got ..." error from `asyncSingleProc`, right? This is what we will get: 276 277 ```bash 278 Error: Expected return type of 'Future' got 'Handle1' 279 ``` 280 281 Knowing the implementation of the `asyncSingleProc` macro, we know that `InternalRaisesFuture[void, (CancelledError,)]` would work just fine as the return type: 282 283 ```nim 284 proc newHandle1(): InternalRaisesFuture[void, (CancelledError,)] {.async: (raw: true, raises: [CancelledError]).} = 285 newFuture[void]() 286 ``` 287 288 but not: 289 290 ```nim 291 proc newHandle1(): Future[void].Raises(CancelledError) {.async: (raw: true, raises: [CancelledError]).} = 292 newFuture[void]() 293 ``` 294 295 Thus we have to stick to `Future` as the return type if we want to stick to the public interface: 296 297 ```nim 298 proc newHandle1(): Future[void] 299 {.async: (raw: true, raises: [CancelledError]).} = 300 newFuture[void]() 301 ``` 302 303 It actually does not matter that we specify `Future[void]` as the return type (yet, it has to be `Future`): the actual return type of `newFuture` and of the `newHandle1` proc will be `InternalRaisesFuture[void, (CancelledError,)]` thanks to the `assert: (raw: true, raises: [CancelledError])`. 304 305 It would be nice if we can create a more generic version of `newHandle`, so that we do not have to create a new one for each single raising future type. Ideally, we would like this generic to also allow us handling the raised exceptions accordingly. 306 307 Using just plain generic does not seem to allow us passing the list of exception types so that it lands nicely in the `raises: [...]` attribute: 308 309 310 ```nim 311 proc newRaisingFuture[T, E](): Future[T] {.async: (raw: true, raises: [E]).} = 312 newFuture[T]() 313 ``` 314 315 With this we can pass a single exception type as E. To pass a list of exceptions we can use a template: 316 317 ```nim 318 template newRaisingFuture[T](raising: typed): untyped = 319 block: 320 proc wrapper(): Future[T] {.async: (raw: true, raises: raising).} = 321 newFuture[T]() 322 wrapper() 323 ``` 324 325 With the `newRaisingFuture` template we can simplify our example to get: 326 327 ```nim 328 proc newSomeType(name: string): SomeType = 329 let t = SomeType( 330 name: name, 331 handle1: newRaisingFuture[void]([CancelledError]), 332 handle2: newRaisingFuture[void]([CancelledError, MyError]), 333 ) 334 t 335 ``` 336 337 Perhaps, a more elegant solution would be to use an IIFE (Immediately Invoked Function Expression), e.g.: 338 339 ```nim 340 (proc (): Future[void] 341 {.async: (raw: true, raises: [CancelledError, MyError]).} = 342 newFuture[void]())() 343 ``` 344 345 so that we can create a raising future instance like this: 346 347 ```nim 348 let ff = ( 349 proc (): Future[void] 350 {.async: (raw: true, raises: [CancelledError, MyError]).} = 351 newFuture[void]())() 352 )() 353 ``` 354 355 Unfortunately, this will fail with error similar to this one: 356 357 ```bash 358 Error: type mismatch: got 'Future[system.void]' for ' 359 newFutureImpl(srcLocImpl("", (filename: "raisingfutures.nim", line: 264, 360 column: 19).filename, (filename: "raisingfutures.nim", line: 264, 361 column: 19).line), {})' but expected 362 'InternalRaisesFuture[system.void, (CancelledError, MyError)]' 363 ``` 364 365 To see what happened, we can use the `-d:nimDumpAsync` option when compiling, e.g.: 366 367 ```bash 368 nim c -r -o:build/ --NimblePath:.nimble/pkgs2 -d:nimDumpAsync raisingfutures.nim 369 ``` 370 371 This option will print us the expanded `async` macro, where we can find that our call expanded to: 372 373 ```nim 374 proc (): InternalRaisesFuture[void, (CancelledError, MyError)] 375 {.raises: [], gcsafe.} = 376 newFuture[void]() 377 ``` 378 379 This obviously misses the definition of the `InternalRaisesFutureRaises` type before calling `newFuture`, which would change the behavior of the `newFuture` call so that instead of returning a regular `Future[void]` it would return `InternalRaisesFuture[system.void, (CancelledError, MyError)]`. The same function, evaluated as regular proc (and not as lambda call) would take the following form: 380 381 ```nim 382 proc (): InternalRaisesFuture[seq[int], (CancelledError, MyError)] 383 {.raises: [], gcsafe.} = 384 type InternalRaisesFutureRaises {.used.} = (CancelledError, ValueError,) 385 newFuture[seq[int]]() 386 ``` 387 388 Looking again into the `chronos/internal/asyncmacro.nim`: 389 390 ```nim 391 if raw: # raw async = body is left as-is 392 if raises != nil and prc.kind notin {nnkProcTy, nnkLambda} and not isEmpty(prc.body): 393 # Inject `raises` type marker that causes `newFuture` to return a raise- 394 # tracking future instead of an ordinary future: 395 # 396 # type InternalRaisesFutureRaises = `raisesTuple` 397 # `body` 398 prc.body = nnkStmtList.newTree( 399 nnkTypeSection.newTree( 400 nnkTypeDef.newTree( 401 nnkPragmaExpr.newTree( 402 ident"InternalRaisesFutureRaises", 403 nnkPragma.newTree(ident "used")), 404 newEmptyNode(), 405 raises, 406 ) 407 ), 408 prc.body 409 ) 410 411 ``` 412 413 we see the condition: 414 415 ```nim 416 if raises != nil and prc.kind notin {nnkProcTy, nnkLambda} and not isEmpty(prc.body): 417 ``` 418 419 Unfortunately, in our case `prc.kind` is `nnkLambda`, and so the above mentioned type infusion will not happen... 420 421 > I do not know why it is chosen to be like this... 422 423 Thus, if we would like to use IIFE, we do have to use an internal function from `chronos/internal/asyncfutures.nim`: 424 425 ```nim 426 (proc (): Future[void] 427 {.async: (raw: true, raises: [CancelledError, MyError]).} = 428 newInternalRaisesFuture[void, (CancelledError, MyError)]())() 429 ``` 430 431 This call will work, and we can then "hide" the internal primitive in a macro. Below I show the macro, we can use to conveniently create *raising futures* using the IIFE: 432 433 ```nim 434 macro newRaisingFuture(T: typedesc, E: typed): untyped = 435 let 436 baseType = T.strVal 437 e = 438 case E.getTypeInst().typeKind() 439 of ntyTypeDesc: @[E] 440 of ntyArray: 441 for x in E: 442 if x.getTypeInst().typeKind != ntyTypeDesc: 443 error("Expected typedesc, got " & repr(x), x) 444 E.mapIt(it) 445 else: 446 error("Expected typedesc, got " & repr(E), E) 447 448 let raises = if e.len == 0: 449 nnkBracket.newTree() 450 else: 451 nnkBracket.newTree(e) 452 let raisesTuple = if e.len == 0: 453 makeNoRaises() 454 else: 455 nnkTupleConstr.newTree(e) 456 457 result = nnkStmtList.newTree( 458 nnkCall.newTree( 459 nnkPar.newTree( 460 nnkLambda.newTree( 461 newEmptyNode(), 462 newEmptyNode(), 463 newEmptyNode(), 464 nnkFormalParams.newTree( 465 nnkBracketExpr.newTree( 466 newIdentNode("Future"), 467 newIdentNode(baseType) 468 ) 469 ), 470 nnkPragma.newTree( 471 nnkExprColonExpr.newTree( 472 newIdentNode("async"), 473 nnkTupleConstr.newTree( 474 nnkExprColonExpr.newTree( 475 newIdentNode("raw"), 476 newIdentNode("true") 477 ), 478 nnkExprColonExpr.newTree( 479 newIdentNode("raises"), 480 raises 481 ) 482 ) 483 ) 484 ), 485 newEmptyNode(), 486 nnkStmtList.newTree( 487 nnkCall.newTree( 488 nnkBracketExpr.newTree( 489 newIdentNode("newInternalRaisesFuture"), 490 newIdentNode(baseType), 491 raisesTuple 492 ) 493 ) 494 ) 495 ) 496 ) 497 ) 498 ) 499 ``` 500 501 Now, creating a raising future is quite elegant: 502 503 ```nim 504 proc newSomeType(name: string): SomeType = 505 let t = SomeType( 506 name: name, 507 handle1: newRaisingFuture(void, CancelledError), 508 handle2: newRaisingFuture(void, [CancelledError, MyError]), 509 ) 510 t 511 ``` 512 513 ### Using raising futures types 514 515 While `Future[...].Raising(...).init()` provides us with quite elegant (although verbose) interface to create raising futures, it seems to display some subtle limitations. 516 517 To demonstrate them, let start with the following, quite innocent looking proc: 518 519 ```nim 520 proc waitHandle[T](h: Future[T]): Future[T] 521 {.async: (raises: [CancelledError]).} = 522 await h 523 ``` 524 525 Now, let's try to call it passing a raising future as an argument: 526 527 ```nim 528 let handle = newRaisingFuture(int, [CancelledError]) 529 handle.complete(42) 530 echo waitFor waitHandle(handle) 531 ``` 532 533 > [!info] 534 > In the examples I am using our macro - just as an example, and it is also shorter to type than `Future[...].Raising(...).init()` 535 536 The compilation will fail with the following error: 537 538 ```bash 539 Error: cast[type(h)](chronosInternalRetFuture.internalChild).internalError can raise an unlisted exception: ref CatchableError 540 ``` 541 542 First, realize that we are passing `InternalRaisesFuture[void, (CancelledError,)]` as `Future[void]`. Because we have that: 543 544 ```nim 545 type InternalRaisesFuture*[T, E] = ref object of Future[T] 546 ``` 547 548 it will not cause any troubles. For `Future[T]`, the following version of `await` will be called: 549 550 ```nim 551 template await*[T](f: Future[T]): T = 552 ## Ensure that the given `Future` is finished, then return its value. 553 ## 554 ## If the `Future` failed or was cancelled, the corresponding exception will 555 ## be raised instead. 556 ## 557 ## If the `Future` is pending, execution of the current `async` procedure 558 ## will be suspended until the `Future` is finished. 559 when declared(chronosInternalRetFuture): 560 chronosInternalRetFuture.internalChild = f 561 # `futureContinue` calls the iterator generated by the `async` 562 # transformation - `yield` gives control back to `futureContinue` which is 563 # responsible for resuming execution once the yielded future is finished 564 yield chronosInternalRetFuture.internalChild 565 # `child` released by `futureContinue` 566 cast[type(f)](chronosInternalRetFuture.internalChild).internalRaiseIfError(f) 567 when T isnot void: 568 cast[type(f)](chronosInternalRetFuture.internalChild).value() 569 else: 570 unsupported "await is only available within {.async.}" 571 ``` 572 573 The reason for exception is: 574 575 ```nim 576 cast[type(f)](chronosInternalRetFuture.internalChild).internalRaiseIfError(f) 577 ``` 578 579 which is: 580 581 ```nim 582 macro internalRaiseIfError*(fut: FutureBase, info: typed) = 583 # Check the error field of the given future and raise if it's set to non-nil. 584 # This is a macro so we can capture the line info from the original call and 585 # report the correct line number on exception effect violation 586 let 587 info = info.lineInfoObj() 588 res = quote do: 589 if not(isNil(`fut`.internalError)): 590 when chronosStackTrace: 591 injectStacktrace(`fut`.internalError) 592 raise `fut`.internalError 593 res.deepLineInfo(info) 594 res 595 ``` 596 597 Thus, this will cause the error we see above. 598 599 >[!info] 600 > Notice that it is not *casting* that causes an error. 601 602 We could cast to `InternalRaisesFuture` before calling `await`, but although we can often be pretty sure that what we are doing is right, it would be better to avoid downcasting if possible. 603 604 Thus, in our `waitHandle` it would be better that we capture the correct type. Fortunately, this is possible, although not obvious. 605 606 Unfortunately, the following will not work as the type of the argument `h`: 607 608 ```nim 609 Future[T].Raising([CancelledError]) 610 Raising[T](Future[T],[CancelledError]) 611 Raising(Future[T],[CancelledError]) 612 ``` 613 614 Sadly, original `Raising` macro is not flexible enough to handle complications of the generics. 615 616 We may like to define a custom type, e.g.: 617 618 ```nim 619 type 620 Handle[T] = Future[T].Raising([CancelledError]) 621 ``` 622 623 Unfortunately, `Raising` macro is again not flexible enough to handle this. Doing: 624 625 ```nim 626 type 627 Handle[T] = Raising(Future[T], [CancelledError]) 628 ``` 629 630 looks more promising, but trying to use `Handle[T]` as type for `h` in our `waitHandle` fails. 631 632 What works is `auto`: 633 634 ```nim 635 proc waitHandle[T](_: typedesc[Future[T]], h: auto): Future[T] {.async: (raises: [CancelledError]).} = 636 await h 637 638 let handle = newRaisingFuture(int, [CancelledError]) 639 handle.complete(42) 640 echo waitFor Future[int].waitHandle(handle) 641 ``` 642 643 Finally, I have experimented a bit with modifying the original `Rasing` macro from chronos, to see if I can make it a bit more permissive. In particular, where `Future[...].Raising(...)` seem to have limitations are generic types, e.g.: 644 645 ```nim 646 SomeType2[T] = object 647 name: string 648 # will not compile 649 handle: Future[T].Raising([CancelledError]) 650 ``` 651 652 Here is a version that works: 653 654 ```nim 655 from pkg/chronos/internal/raisesfutures import makeNoRaises 656 657 macro RaisingFuture(T: typedesc, E: typed): untyped = 658 let 659 baseType = T.strVal 660 e = 661 case E.getTypeInst().typeKind() 662 of ntyTypeDesc: @[E] 663 of ntyArray: 664 for x in E: 665 if x.getTypeInst().typeKind != ntyTypeDesc: 666 error("Expected typedesc, got " & repr(x), x) 667 E.mapIt(it) 668 else: 669 error("Expected typedesc, got " & repr(E), E) 670 # @[] 671 672 let raises = if e.len == 0: 673 makeNoRaises() 674 else: 675 nnkTupleConstr.newTree(e) 676 677 result = nnkBracketExpr.newTree( 678 ident "InternalRaisesFuture", 679 newIdentNode(baseType), 680 raises 681 ) 682 ``` 683 684 We can then do: 685 686 ```nim 687 SomeType2[T] = object 688 name: string 689 # will not compile 690 handle: RaisingFuture(T, [CancelledError]) 691 ``` 692 693 We finish this note with some examples of using the `RasingFuture` and `newRaisingFuture` macros: 694 695 ```nim 696 type 697 MyError = object of CatchableError 698 699 SomeType = object 700 name: string 701 handle1: RaisingFuture(void, CancelledError) 702 handle2: RaisingFuture(void, [CancelledError, MyError]) 703 handle3: RaisingFuture(int, [CancelledError]) 704 705 SomeType2[T] = object 706 name: string 707 handle: RaisingFuture(T, [CancelledError]) 708 709 710 proc newSomeType(name: string): SomeType = 711 let t = SomeType( 712 name: name, 713 handle1: newRaisingFuture(void, CancelledError), 714 handle2: newRaisingFuture(void, [CancelledError, MyError]), 715 handle3: newRaisingFuture(int, [CancelledError]), 716 ) 717 t 718 719 proc newSomeType2[T](name: string): SomeType2[T] = 720 let t = SomeType2[T]( 721 name: name, 722 handle: newRaisingFuture(T, CancelledError), 723 ) 724 t 725 726 let someTypeInstance = newSomeType("test") 727 728 echo typeof(someTypeInstance.handle1) 729 echo typeof(someTypeInstance.handle2) 730 echo typeof(someTypeInstance.handle3) 731 732 let someType2Instance = newSomeType2[int]("test2") 733 echo typeof(someType2Instance.handle) 734 735 proc waitHandle[T](_: typedesc[Future[T]], h: auto): Future[T] {.async: (raises: [CancelledError]).} = 736 await h 737 738 proc waitHandle2[T](_: typedesc[Future[T]], h: RaisingFuture(T, [CancelledError])): Future[T] {.async: (raises: [CancelledError]).} = 739 return await h 740 741 742 someTypeInstance.handle1.complete() 743 744 waitFor Future[void].waitHandle(someTypeInstance.handle1) 745 echo "done 1" 746 747 someType2Instance.handle.complete(42) 748 749 echo waitFor Future[int].waitHandle(someType2Instance.handle) 750 echo "done 2" 751 752 let handle = newRaisingFuture(int, CancelledError) 753 handle.complete(43) 754 echo waitFor Future[int].waitHandle2(handle) 755 echo "done 3" 756 ``` 757 758 You can also find the source files on GitHub: 759 760 - [raisingfutures.nim](https://gist.github.com/marcinczenko/cb48898d24314fbdebe57fd815c3c1be#file-raisingfutures-nim) 761 - [raisingfutures2.nim](https://gist.github.com/marcinczenko/c667fad0b70718d6f157275a2a7e7a93#file-raisingfutures2-nim)