Skip to content

(almost) All the ways to handle errors in C++ — Transcript

Comprehensive overview of error handling in C++ with practical examples and trade-offs for recoverable and unrecoverable errors.

Key Takeaways

  • C++ does not inherently distinguish between recoverable and unrecoverable errors, making error handling nuanced.
  • Undefined behavior from unchecked errors can corrupt program state and cause unpredictable failures.
  • Validating inputs and using proper error handling techniques is critical for program reliability.
  • Modern C++ features like std::optional and std::expected improve error handling but require trade-offs.
  • Error handling strategies should be chosen based on domain requirements such as safety and predictability.

Summary

  • Introduction to error handling in C++ and its importance compared to real life.
  • Distinction between unrecoverable and recoverable errors and their implications.
  • Use of a simple puzzle game example to illustrate error handling concepts in C++.
  • Demonstration of undefined behavior caused by out-of-bounds access leading to unrecoverable errors.
  • Discussion on the unpredictability and dangers of undefined behavior in C++ programs.
  • Importance of validating user input to prevent errors and maintain program stability.
  • Overview of common C++ error handling techniques including exceptions and error codes.
  • Mention of modern C++ features like std::optional and std::expected for safer error handling.
  • Trade-offs involved in different error handling methods and their applicability in various domains.
  • Encouragement for feedback and further exploration of error handling strategies.

Full Transcript — Download SRT & Markdown

00:00
Speaker A
When writing C++ code, much like in real life, we don't always get what we want.
00:05
Speaker A
The good news is that unlike in real life, C++ comes packed with the tools that help us prepare for not getting what we want. Today, we're talking about error handling, what options we have, and which trade-offs they come with. Ah,
00:17
Speaker A
and I can already smell the torches and hear the scraping of the pitchforks that people will inevitably bring to punish me for all the controversial statements that I'm about to make. What could go wrong, eh?
00:28
Speaker A
[Applause] [Music] Jokes aside, this is definitely not a one-size-fits-all topic. C++ is huge, powerful, and used across every domain imaginable for a long, long time. My perspective comes from domains like robotics and automotive where predictability and safety are of highest
00:51
Speaker A
importance. What works for these domains may not work for everybody else. However, I believe that what we talk about today fits many setups with minimal adaptation and is grounded in at least relatively sane reasoning. Where possible, I'll try to mention multiple
01:05
Speaker A
possible options. And if I do miss an important one, please let me know in the comments. And on this note, your comments and likes are the only ways I get any feedback from speaking into the void. So, please do not shy away from
01:18
Speaker A
sharing what you think, positive or negative. I value every nugget of feedback I can get. Now back to error handling. Before we go into how to handle errors, however, let's clarify what we mean when we think about an
01:30
Speaker A
error in programming. At the highest level, an error is what happens when a program doesn't produce the result we expect. I tend to think of errors belonging to one of two broad groups.
01:41
Speaker A
Unrecoverable errors where a program reaches a state in which recovery is impossible or meaningless and recoverable errors where the program can detect that something went wrong and has ways to proceed following an alternative path. Some languages like Rust bake this
01:56
Speaker A
distinction directly into the language design. C++ doesn't, making the topic of error handling slightly more nuanced.
02:03
Speaker A
But for my money, this classification, while not universal, is still quite useful. So let me present my case and talk a bit more in depth about these kinds of errors and the intuition behind them. We have a lot to talk about and, uh,
02:18
Speaker A
to not get lost, I would like to introduce a small example that will guide us and help illustrate all of the concepts we are talking about today. To this end, let's model a simple puzzle game. In this game, a player starts with
02:30
Speaker A
a vector of numbers generated for them. In the end of the game, this vector gets compared to some also generated reference vector. The vectors are compared element by element and the player gets nothing if the elements are equal, a minus one if their number is
02:45
Speaker A
smaller than the reference one, and a plus one if their number is larger. If the resulting sum of these scores is positive, the player wins the game and loses otherwise. To make it an actual game, we need to give the player at
02:57
Speaker A
least some control over their numbers. So, we allow them to use a certain budget that can be used up to increase any number in their vector. The goal is to use the budget cleverly to win the game. We won't model all of the
03:08
Speaker A
potential complexity of this game, but I urge you to play around with these ideas after watching this lecture and see if you can build something that is actually interesting to play. To start off, we'll probably need a class game that would
03:20
Speaker A
hold reference and player numbers as well as the remaining budget. It also needs a way to print the current state of the game to check if the player won by comparing the player's numbers with the reference ones one by one and
03:33
Speaker A
keeping the score. And to change the player number if there is still budget for this, provided a change entry object, a tiny struct with an index and value. In our case, this change entry must come from somewhere. So we need a way to ask
03:47
Speaker A
the player to provide it. We can encapsulate our user interactions into a function like get next change entry from user, which will print the current state of the game, ask the user for their input, and fill the change entry object using
04:00
Speaker A
this input. To keep this example simple, we implement all of this in one CPP file alongside a simple main function that creates a game object, asks the user to provide a desired change entry, and changes the player's numbers
04:12
Speaker A
accordingly in a loop until the user runs out of budget. Finally, we check if the player has won the game and show them the result. This is not the most elegant code out there, but it is not too far from the style of code that you
04:23
Speaker A
might encounter in the wild. We can build this program as a single executable directly from the command line. Ideally, we should at least unit test all of our functions, but it will serve us as a nice cautionary tale if we
04:35
Speaker A
just give it a playthrough instead. So, we run our executable and are greeted with expected numbers as well as a prompt to change one of our numbers. We see that we hopelessly lose on the first number, but we win on the last number.
04:49
Speaker A
The decisive number here is the second one, which we can increase by 10 from 40 to 50, leading us to win this game.
04:56
Speaker A
Considering that our budget is exactly 10, we provide these numbers to our program and observe what happens. But wait, wait, wait, wait. What's going on here? Why did our number in the second column not change? Why is our budget not
05:10
Speaker A
decreased by 10? Even more strangely, why did the first reference number change to 50? The answer to all of these questions is that we have just encountered an unrecoverable error that manifests itself in wrong values in our memory through
05:24
Speaker A
the virtues of undefined behavior. But what gives? Well, there is a chain of events that caused our values to be changed in ways that we didn't expect.
05:33
Speaker A
The most immediate cause is that the user has mistakenly provided the number they wanted to change, 40, rather than an index of that number. We then did not check that the provided index is within the allowed bounds of the player numbers
05:47
Speaker A
vector and wrote the provided new value under this wrong index. If we correct our mistake by rerunning our game again and providing one as the first input, we win just as we expected to. When we provide 40 as we did the first time,
06:02
Speaker A
our wrong index is far beyond the size of the player numbers vector. And when we write beyond its bounds, we enter the undefined behavior land. What happens next is unpredictable. If we are lucky and the address into which we write does
06:17
Speaker A
not belong to our program, the program crashes. But if we are not lucky, we will rewrite some memory that belongs to our program, potentially corrupting any objects that actually own that memory.
06:28
Speaker A
In this particular example, I picked the values in such a way that the fake index just happens to be equal to a difference in pointers between the player numbers data and the ref numbers data, which then results in us writing directly into
06:42
Speaker A
the first element of the ref numbers vector, resulting in an unexpected update to the reference number. But I want to stress again that if we run the same program on another machine, we will most likely get a different behavior
06:54
Speaker A
altogether. Even the order of ref numbers and player numbers in memory is not guaranteed. Note how during this run on my machine, they do not even follow the order of declaration. What doesn't change is that once the change player
07:07
Speaker A
number if possible method is called with a wrong change entry in our example, all bets are off. We do not have any guarantees on the consistency of the state of our program anymore. Arbitrary objects might have already been
07:20
Speaker A
corrupted, which can lead to random-looking, sporadic failures in seemingly unrelated parts of our program across multiple runs. This obviously becomes even harder to track down when we write more complex programs than our toy example over here. This idea lies at
07:34
Speaker A
the core of what makes this type of error unrecoverable. If the data we try to use
07:49
Speaker A
values that propagate through our program as early as possible and crash as early as possible before any more damage is done. My favorite tool for this is the check macro that can be found in the absile library. To use it,
08:02
Speaker A
we must include the absol.h header file and change our change player number if possible function so that it uses the check macro to check if the index is within bounds. If we run our example now, we will get a crash as soon
08:16
Speaker A
as we call the change player number if possible function that clearly states where this error originates from and which check failed, letting us debug this as easily as possible. To the degree of my knowledge, using check- like macros to check the preconditions
08:31
Speaker A
of functions is widely considered a good practice. These checks can be violated either due to a bug in our program or due to some undefined behavior. Just like in our example from earlier, one concern that people have when thinking
08:45
Speaker A
of using check micros is performance, as these checks stay in the production code we ship and do cost some little time when our program runs. For my money, in most cases, the benefits far outweigh the costs. And unless we've measured
08:58
Speaker A
that we cannot allow the tiny performance hit in a particular place of our code, we should be free to use check for safety against entering the undefined behavior land and saving us days, weeks, or even months of debugging
09:10
Speaker A
issues that are really hard to debug. You might wonder if using check is the only way to help us detect when we are in an inconsistent state. And so I have to talk about one very famous alternative that is often recommended on
09:23
Speaker A
the internet. This alternative is to use assert statement which can be found in the C assert include file. Full disclosure, I'm not a fan of using assert. If you came here with your torches and pitchforks, this is probably
09:36
Speaker A
a good time to get them ready, but let me explain myself before you do anything drastic. You see, assert has one super annoying flaw that makes it impossible for me to recommend it for production code. I've seen so many bugs go
09:49
Speaker A
unnoticed because of this. So let me show what I'm talking about using our game example. First, we can use assert in a very similar way to our check macro. We compile and run our game just as we did before. And if we run our
10:01
Speaker A
program, the assertion triggers. Using assert also crashes our program when the wrong input is provided and shows us where the wrong value was detected. So what is that annoying flaw I've been talking about that makes me dislike assert? Well, you see, all assert
10:15
Speaker A
statements get disabled when a macro and debug is defined. This is a standard macro that controls if the debug symbols get compiled into the binary and gets passed to the compilation command for most release builds as we generally
10:29
Speaker A
don't want debug symbols in the binary we release. So essentially assert almost always does not protect us from undefined behavior in the code we actually deploy. We can easily demonstrate that the asserts indeed get compiled out by adding D and debug flag
10:45
Speaker A
to our compilation command. Running our game now and providing a wrong input index leads to the same undefined behavior we observed before as all of the assertions were compiled out. Not great, right? What makes it even less great is that many people just don't
11:01
Speaker A
know that asserts get disabled like that and are sure that they are protected while they are really not. and check macro just doesn't have these flaws. By the way, we've only covered how we could improve our change player number if
11:14
Speaker A
possible methods of the game class. Do you think our check if player one function would benefit from the same treatment? And of course, hard failures in the programs we ship are not ideal.
11:24
Speaker A
One way to reduce the risk of such failures is to keep the test coverage high for the code we write. Ideally, close to 100% line and branch coverage.
11:33
Speaker A
That means that every line and logical branch gets executed at least once in our test suite which is run regularly and automatically. This way we catch most of the unreoverable errors during development. In some industries like automotive, aviation or medical, this is
11:48
Speaker A
actually a requirement to obtain the certifications necessary for the resulting software to be actually used.
11:54
Speaker A
But unfortunately, despite our best efforts, we cannot completely avoid catastrophic failures in the programs we ship. Even if we do everything right on our side, hardware can still fail and corrupt our memory. One fun example of this is the famous error in the Belgian
12:10
Speaker A
election on the 18th of May 2003, where one political party got 4,96 extra votes due to what is believed to have been a cosmic ray flipping a bit in the position 13 in the memory of the computer, essentially leading to 4,96
12:25
Speaker A
more votes. This visualization is taken from an excellent veritizing video on this topic. Do watch it if you want to know more. With the knowledge that we cannot completely remove the risk of hitting an unreoverable error in production and that we also probably
12:38
Speaker A
don't want our say flight software just randomly dying during operation. In safety critical systems, we often isolate components into separate processes or even separate hardware units. With watchd dogs that can trigger recovery actions if one of our components suddenly crashes. This way we
12:54
Speaker A
can have our cake and eat it at the same time. Using check minimizes the time to failure while our fallback options keep the system safe as a whole even when certain components do fail. That being said, this is a system architecture
13:08
Speaker A
question and this topic is far beyond what I want to talk about today. In most non-safety critical systems, we do not need to think about these failure cases as deeply and we can usually just restart our program in case of a one-off
13:21
Speaker A
failure. But not every error should instantly crash our program, right? Indeed, in our example, the original cause of the error is not a cosmic ray flipping bits of our memory, but the wrong user input. The good thing about
13:35
Speaker A
user inputs is that we can ask the user to correct these without crashing. The type of errors we encounter here can be called recoverable errors. To talk about them, let us focus on the function get next change entry from user. Currently,
13:50
Speaker A
there is no validation of what the user provides as input. But we absolutely can and should perform such validation. So when the player does provide a wrong value, we'd like to somehow know that something went wrong within the get next
14:04
Speaker A
change entry from user function and recover from this. Broadly speaking, we have two strategies of communicating failures like these that have emerged in C++ over the years. The first one is to return a special value from a function.
14:18
Speaker A
And the second one is to throw an exception. Today we'll spend most of our time on the first option. But let's first talk about throwing exceptions and yes, why I think it might not be the best thing we could do. This is yet
14:31
Speaker A
again a good time to get your pitchforks and torches ready. Since C++ 98, we have a powerful machinery of exceptions at our disposal. An exception is essentially just an object of some type typically derived from stood exception class. Such an exception holds the
14:45
Speaker A
information about the underlying failure and can be thrown and caught within a C++ program. In our example, we could throw an object of stood out of range when the user inputs a wrong number index. Throwing an exception interrupts
14:59
Speaker A
the normal program flow. We leave the current scope so all objects allocated in it are automatically destroyed and the program continues with the exceptional flow to find a place where the thrown exception can be handled.
15:10
Speaker A
Speaking of handling exceptions, we can catch them anywhere upstream from the place that they have been thrown from.
15:17
Speaker A
As stood exception is just an object, it can be caught by value or by reference, but it is considered a good practice to catch them by reference. All of the exceptions that derive from stood exception can override the what function
15:28
Speaker A
that we can use to see the cause of the crash. Should we forget to catch an exception, it bubbles up to the very top and terminates our program. To verify this, we could change our catch from catching an out of range to catching a
15:41
Speaker A
say runtime error and see what happens. On paper, all of this looks very neat.
15:45
Speaker A
Essentially, with exceptions, we could forget about the distinction between recoverable and unreoverable errors. If we catch an error, it is a recoverable one. And if we don't, we treat it as unreoverable. This is one of the main arguments from people who like using
16:00
Speaker A
exceptions and don't like the concept of unreoverable errors. and it is a very compelling argument. The other compelling argument is that exceptions are part of the language. So it feels odd not to use them. But there are problems with exceptions that at least
16:15
Speaker A
in some industries we just cannot ignore. There is a really in-depth C++ now talk called exceptions demystified by Andreas Vice that goes into a lot of detail about how exceptions behave. Long story short, both throwing and catching exceptions relies on mechanisms that
16:33
Speaker A
work at runtime and therefore cost execution time. Unfortunately, there are no guarantees on timing or performance of at least some of these operations.
16:41
Speaker A
While in most common scenarios, these operations run fast enough in real time or safety critical code. Such unpredictability is usually unacceptable. Exceptions also arguably make control flow harder to read and reason about. to quote Google C++ stylesheet. Exceptions make the control
16:57
Speaker A
flow of programs difficult to evaluate by looking at code. Functions may return in places you don't expect. This causes maintainability and debugging difficulties. Indeed, an error can propagate across many layers of calls before being caught. It's easy to miss
17:13
Speaker A
what a function might throw, especially if documentation is incomplete or out ofd, which it almost always is.
17:19
Speaker A
Furthermore, the language permits the use of generic catch ellipses blocks. And these make things even more confusing. We end up catching something, but we no longer know what we caught or who threw it at us. I believe that this
17:31
Speaker A
catch ellipses and equivalent constructs are single-handedly responsible for the absolute majority of the very unspecific and unhelpful error messages that we see all over the internet and have probably encountered ourselves multiple times.
17:45
Speaker A
All of these issues led a lot of code bases to ban exceptions altogether. In 2019, isocpp.org org did a survey on this matter and found that about half the respondents could not use exceptions at least in part of their code bases. My
18:00
Speaker A
own experience aligns with these results. Every serious project I've worked on either banned exceptions completely or avoided them in performance critical paths. But then again, I did work in robotics and automotive for the majority of my career. The problem of ensuring that
18:15
Speaker A
using exceptions has an acceptable overhead has quite a vibrant discussion around it with even calls for redesigning exceptions altogether as can be seen in this wonderful talk by Herbs from CPPCON 2019 as well as his corresponding paper on this topic. All
18:31
Speaker A
of these are of course linked in the description to this video. But until the C++ community figures out what to do, we are stuck with many people being unable to use the default error handling mechanism in C++. So the question is
18:44
Speaker A
what do we do now? And now it's time to return to the other option of detecting errors in functions that we hinted at before. Dealing with errors by returning a special value from a function. Here again I would say that there are three
18:58
Speaker A
distinct ways of thinking about it. For some function that might fail, we can keep the return type of the function if it has any and return a special value of this type in case of failure. There is a
19:10
Speaker A
number of issue with returning a special value from a function without using a special return type. As an illustration in our get next change entry from user function, a naive choice would be to return a value initialized change entry
19:22
Speaker A
object if the user provided a wrong number index. This object would essentially hold zeros for its index and value entries. However, as you might imagine, a pair of zeros is a completely valid, if slightly useless, change entry. How do we disambiguate this value
19:37
Speaker A
from a valid one? Many other real world functions will face the same issues which makes this method not particularly useful in practice. Returning an error code instead solves at least a couple of these issues. It is fast and reliable
19:49
Speaker A
and we can design our software with different error codes in mind so that the reason for the failure is also communicated to us. This is also still the prevalent way of handling errors in C as far as I know as well as in some
20:01
Speaker A
libraries that we can find in the wild. So there is some merit to this method.
20:05
Speaker A
However, if our function actually must return a value, which most of the functions do, the only way to use error codes is to change its return type to the type that our error codes have like int which forces us to provide an
20:18
Speaker A
additional output parameter to our function like a reference to a change entry called result which we subsequently fill inside of the body of our function. The main issue with this from my point of view is that it is
20:29
Speaker A
clunky, mixes input and output parameters in the signature and limits functional composition. Consider how we would use this function. Here we can detect that something went wrong. And if we have some function get reason that converts an error code to string, we
20:41
Speaker A
even know what went wrong. But we have to create change entry object before calling the get next change entry from user function. Furthermore, this object cannot be const which goes against everything we've been talking about in this series of C++ lectures until now.
20:56
Speaker A
On top of all of this, nowadays the compilers are able to perform return value optimization or RVO for values returned from a function essentially skipping the function call altogether and constructing the needed value in place. This functionality is however
21:11
Speaker A
limited when using input output parameters. So clearly there are some issues with this method too. I believe it has its merits sometimes but there has to be a reason for using it and we must measure the performance well. I
21:25
Speaker A
believe that there is a better way though. With C++ 17, we gain stood optional with which we can express that a function might return a value by returning an object of stood optional change entry type. We can create this
21:36
Speaker A
object either empty holding a so-called stood null opt or from a valid change entry. We can use our newly returned optional object in an if statement to find out if it actually holds a valid change entry. If the object does not
21:49
Speaker A
hold a value, we show an error and continue asking the user to provide better input. But if the object does hold a value, we can get to it by calling its value method or using a dreferencing operator star and arrow
22:02
Speaker A
just like we did with pointers. The presence or absence of a value is encoded into the type itself. No more guessing, no more relying on magic return values or input output parameters and the code is very short and neat. But
22:14
Speaker A
we've again lost the capability of knowing what went wrong. We just know that something has not gone to plan.
22:21
Speaker A
There is simply no place to store the reason. But we are still interested in a reason for our failures. Enter stood expected coming in C++ 23. And if you'd like to hear somebody more entertaining than myself talk about why we have it,
22:34
Speaker A
watch this fantastic talk by Andre Alexandresco. It is one of my favorite talks ever. It is both informative and entertaining in an equal measure.
22:42
Speaker A
Anyway, with stood expected, we could do the same things we could do with stood optional and more by changing our function accordingly. Essentially stood expected holds one of two values that have two potentially different types. An expected change entry in our case and an
22:58
Speaker A
unexpected one uh say stood string in our tiny example. Now we can return either a valid result or an error message. Using it is also quite neat and is actually very similar to how we used the stood optional. The only difference
23:11
Speaker A
is that uh we can now get the error from the object we return from get next change entry from user if it holds one.
23:18
Speaker A
This has all the benefits that we mentioned before. The signature of our function clearly states that it might fail. The error if it happens needs to be dealt with explicitly by the caller.
23:29
Speaker A
Everything happens in deterministic time with no unpredictable runtime overhead. And it also works for functions that return void. There is just one tiny issue that spoils our fun. As you've probably noticed, most of the things that we've covered until now in this C++
23:44
Speaker A
course targeted C++ 17 as a recent enough but also widely enough used standard. Unfortunately, stood expected is only available from C++ 23 on. But there is a solution to this. We can use an open- source TL expected as a drop-in
23:59
Speaker A
replacement for code bases that don't yet adopt C++ 23. One final thing I want to talk about is performance considerations when using stood optional and stood expected. Both stood optional and stood expected are implemented using union-like storage internally meaning
24:15
Speaker A
the value and error share memory with the bigger type defining the amount of memory allocated. Note that we should not use union directly in our code but a number of standard classes like stood expected stood optional or stood variant
24:29
Speaker A
that we'll talk about soon use it under the hood. In the case of stood optional, this does not play much of a difference as the error type is a tiny stood null opt type. But for stood expected, the
24:40
Speaker A
error type affects the size of the object even when we're returning a success. So this is something to avoid.
24:47
Speaker A
If we do this, every return now has the size of the larger type. Mostly I've seen people using stood error code, stood string or custom enums as an error type in stood expected. All of these are relatively efficient and have a small
25:01
Speaker A
stack memory footprint. If the code base you work in already has a rule for what types to use as error types instead expected follow it otherwise experiment with the options I just mentioned and find one that works best for your
25:14
Speaker A
current circumstances. And there's also one quirk with how stood optional and stood expected types interact with the return value optimization RVO and named return value optimization NRVO which we already mentioned before. These topics are quite nuanced and I don't want to go
25:30
Speaker A
into many details here but in case we care about the performance of our code that uses stood optional or stood expected we'll have to uglify our code a little bit. For more details, see this short video by Jason Turner where he
25:43
Speaker A
covers all of these situations in depth. And I believe that this concludes a more or less complete overview of how to deal and how not to deal with errors in modern C++. As a short summary, I hope that I could convince you that these are
25:57
Speaker A
some sane suggestions. Use check and similar macros for dealing with unreoverable errors like programming bugs or contract violations in order to fail as fast as possible when they are encountered. Keep the test coverage of the code high to reduce chances of
26:11
Speaker A
crashing in the released code. Use stood optional as a return type when a value might be missing due to a recoverable error and the reason for this failure is not important. Use stood expected for functions that return void but still can
26:24
Speaker A
fail or when a reason for failure is needed to be able to recover from it.
26:28
Speaker A
Avoid exceptions in time critical or safety critical systems due to their non-deterministic runtime overhead and avoid old error handling mechanisms like returning error codes when possible. All in all, the direction that we seem to be following as a community is to make
26:44
Speaker A
failure explicit and force the caller to handle it as close to the call site as possible or explicitly propagated further. This leads to cleaner, safer, and more maintainable code. Now, this is the end of today's lecture. Thanks for
26:57
Speaker A
watching everybody. If you find these videos useful, just let them play fully through to the end so that YouTube shows them to more people and maybe watch one of these other videos of mine once you're at it. As we spoke about how
27:09
Speaker A
important test coverage is, sounds like this video on how to set up Google test framework with CMake might come in handy. Or in case you'd like to refresh how dynamic polymorphism works and why it takes time at runtime, then please
27:22
Speaker A
click this video instead. And see you in the next one. Bye.
Topics:C++error handlingunrecoverable errorsrecoverable errorsundefined behaviorexceptionsstd::optionalstd::expectedinput validationprogram safety

Answers

Frequently Asked Questions

What are the two broad categories of errors discussed in C++?

The video distinguishes between unrecoverable errors, where recovery is impossible or meaningless, and recoverable errors, where the program can detect the error and proceed via an alternative path.

Why is undefined behavior dangerous in C++ error handling?

Undefined behavior can corrupt memory and program state unpredictably, causing sporadic failures that are hard to diagnose and making errors unrecoverable.

How can modern C++ features improve error handling?

Features like std::optional and std::expected provide safer ways to represent and handle errors explicitly, improving code clarity and robustness, though they come with some trade-offs in complexity and performance.

Get More with the Söz AI App

Transcribe recordings, audio files, and YouTube videos — with AI summaries, speaker detection, and unlimited transcriptions.

Or transcribe another YouTube video here →