diff --git a/www/docs/errors.mdx b/www/docs/errors.mdx index 86f95388f..2871f781c 100644 --- a/www/docs/errors.mdx +++ b/www/docs/errors.mdx @@ -10,23 +10,21 @@ react to failure conditions. ## Tasks as Promises -Every Effection operation runs within a Task, when you call `run` or use the -`spawn` operation, you create a task. For each `yield` point, Effecion creates -a task for you. Every Task evaluates to one of three possible states, the task -can either become `completed`, meaning it finished normally, or it can become -`errored` meaning the task itself or one of its descendants had an error -thrown, or it can become `halted`, meaning that the evalutation of the task was -stopped before it finished. +Whenever you call `run` or use the `spawn` operation, Effection +creates a [`Task`][task] for you. This value is a handle that lets you +both respond to the operation's outcome as well as stop its execution. -We have seen that tasks can act like a `Promise` and that this interface can -be used to integrate Effection code into promise based or async/await code: +As we have seen, a Task is not only an Operation that yields the +result of the operation it is running, it is also a promise that can +be used to integrate Effection code into promise based or async/await +code. That promise will resolve when the operation completes successfully: ``` typescript -import { run } from 'effection'; +import { run, sleep } from 'effection'; async function runExample() { let value = await run(function*() { - yield sleep(1000); + yield* sleep(1000); return "world!"; }); @@ -34,10 +32,8 @@ async function runExample() { } ``` -Tasks can also act like a [Future][], which can be useful sometimes. - -Of course if we throw an error inside of the task, then the task's promise -also becomes rejected: +By the same token, if a task's operation results in an error, then the +task's promise also becomes rejected: ``` typescript import { run } from 'effection'; @@ -53,16 +49,20 @@ async function runExample() { } ``` -When treating a task as a promise, if the task becomes halted, the promise is -rejected with a special halt error: +However, when an operation is halted, it will never produce a value, +nor will it ever raise an error. And, because it will produce neither +a positive nor negative outcome, it is an error to `await` the result +of a halted operation. + +When this happens, the promise is rejected with a special halt error: ``` typescript -import { run } from 'effection'; +import { run, suspend } from 'effection'; async function runExample() { try { - let task = run(); - task.halt(); + let task = run(supsend); + await task.halt(); await task; } catch(err) { console.log("got error", err.message) // => "got error halted" @@ -70,34 +70,38 @@ async function runExample() { } ``` +Notice how it is not an error to `await` the halt operation itself, only to +`await` the outcome of a halted operation. + ## Error propagation One of the key principles of structured concurrency is that when a child fails, the parent should fail as well. In Effection, when we spawn a task, that task -becomes linked to its parent. When the child task becomes `errored`, it will -also cause its parent to become `errored`. +becomes linked to its parent. When the child operation fails, it will +also cause its parent to fail. This is similar to the intuition you've built up about how synchronous code works: if an error is thrown in a function, that error propagates up the stack and causes the entire stack to fail, until someone catches the error. -One of the innovations of async/await code over plain promises and callbacks, -is that you can use regular error handling with `try/catch`, instead of using special -error handling constructs. This makes asynchronous code look and feel more like -regular synchronous codes. The same is true in Effection, we can use `try/catch` +One of the innovations of async/await code over plain promises and +callbacks, is that you can use regular error handling with +`try/catch`, instead of using special error handling constructs. This +makes asynchronous code look and feel more like regular synchronous +code. The same is true in Effection where we can use a regular `try/catch` to deal with errors. ``` typescript import { main, sleep } from 'effection'; function* tickingBomb() { - yield sleep(1000); + yield* sleep(1000); throw new Error('boom'); } -main(function*() { +await main(function*() { try { - yield tickingBomb() + yield* tickingBomb() } catch(err) { console.log("it blew up:", err.message); } @@ -108,22 +112,22 @@ However, note that something interesting happens when we instead `spawn` the `tickingBomb` operation: ``` typescript -import { main } from 'effection'; +import { main, suspend } from 'effection'; import { tickingBomb } from './ticking-bomb'; -main(function*() { - yield spawn(tickingBomb()); +await main(function*() { + yield* spawn(tickingBomb); try { - yield; // sleep forever + yield* suspend(); // sleep forever } catch(err) { console.log("it blew up:", err.message); } }); ``` -You might be surprised that we do *not* enter the catch handler here! Instead, +You might be surprised that we do *not* enter the catch handler here. Instead, our entire main task just fails. This is by design! We are only allowed to -catch errors thrown by whatever we yield to directly, not by any spawned +catch errors thrown by whatever we yield to directly, _not_ by any spawned children or resources running in the background. This makes error handling more predictable, since our catch block will not receive errors from any background task, we're better able to specify which errors we actually want to deal with. @@ -135,71 +139,20 @@ we need to introduce an intermediate task which allows us to bring the error int the foreground. We call this pattern an "error boundary": ``` typescript -import { main } from 'effection'; +import { main, call, spawn, suspend } from 'effection'; import { tickingBomb } from './ticking-bomb'; main(function*() { try { - yield function*() { // error boundary - yield spawn(tickingBomb()); // will blow up in the background - yield; // sleep forever - } + yield* call(function*() { // error boundary + yield* spawn(tickingBomb); // will blow up in the background + yield* suspend(); // sleep forever + }); } catch(err) { console.log("it blew up:", err.message); } }); ``` -## Stack traces - -We have already seen using the `main` entry point to run our code when we build -our entire application using Effection. One advantage of `main` over `run` is that -when our operation fails, we exit the program with a proper failure exit code. Additionally -`main` prints a nicely formatted stack trace for us on failure: - -![Error when using main](/images/no-main-error.png) - -We can see that in addition to the regular stack trace of our program, we also -receive an "Effection trace". This gives some context on where the error -occurred within the structure of our Effection code. To make this as useful as -possible, you can apply [Labels][] to your operations, which will be shown in -this trace. - -## MainError - -There are cases where we want the program to exit, but it might be due to user -error, rather than an internal failure. In this case we might not want to print -a stack trace. For example, if we're building a CLI which reads a file, and the -file that the user has specified does not exist, then we might want to show a -message to the user, but there is no need to show a stack trace. Additonally, -we might want to set a specific exit code. - -Effection ships with a special error type, `MainError`, which works together -with `main` for these types of situations: - -``` typescript -import { main, MainError } from 'effection'; -import { promises as fs } from 'fs'; - -const { readFile } = fs; - -main(function*() { - let fileName = process.argv[2]; - try { - let content = yield readFile(fileName); - console.log(content.reverse().toString()); - } catch(err) { - throw new MainError({ message: `no such file ${fileName}`, exitCode: 200 }); - } -}); -``` - -When using `MainError` the stack trace will not be printed and the exit code -specified in the error will be used. This is what it looks like when we try to -read a file which does not exist: - -![Using main error](/images/main-error.png) - -[Future]: /docs/guides/futures -[Resource]: /docs/guides/resources -[Labels]: /docs/guides/labels +[Resource]: ./resources +[task]: https://deno.land/x/effection/mod.ts?s=Task