Let’s make a gzipped file and see what’s in it. We’ll keep it simple: just write 8 ‘a’s to a file.
$ echo "aaaaaaaa" > test.out
$ xxd test.out
00000000: 6161 6161 6161 6161 0a aaaaaaaa.
As we can see, our file is 9 bytes long. We have 8 ‘a’ bytes written, plus a Line Feed (LF) character written at the end.
Let’s make the gzip file now. We’ll do gzip -1
, since that will use the fastest compression mode and give us more things to talk about.
$ gzip -1 test.out
$ xxd test.out.gz
00000000: 1f8b 0808 bf35 6a61 0403 7465 7374 2e6f .....5ja..test.o
00000010: 7574 004b 4c84 002e 00b6 66d7 ad09 0000 ut.KL.....f.....
00000020: 00
Disclaimer: I made this post as a learning exercise, and any mistakes I make here are my own. I enjoy low level programming but do web dev at Teams as my day job.
gzip specific info
The first few bytes are quite straightforward:
1f8b
- “magic”, hardcoded gzip header08
- Signifies DEFLATE compression method08(00001000)
- bit 3 is set, so there will be a filenamebf35 6a61
- timestamp of UTC Saturday, October 16, 2021 2:15:27 AM04
- compressor used fastest algo (that’s what the -1 was for)03
- Unix operating system (useful for LF/CLRF)
The next few bytes are the filename:
74 65 73 74 2e 6f 75 74 00
t e s t . o u t NUL
The deflated data
The data starts from byte 0x13, with 4b. To decode, we’ll need to see the individual bits since DEFLATE packs the information in bits that can cross the byte boundary. It’s not uncommon to have codes that are 5 bits or 9 bits.
$ xxd -s 19 -b test.out.gz
00000013: 01001011 01001100 10000100 00000000 00101110 00000000 KL....
00000019: 10110110 01100110 11010111 10101101 00001001 00000000 .f....
0000001f: 00000000 00000000
Let’s break it down. Unfortunately, xxd prints out the bytes one by one, from MSB to LSB. But in gzip, the bytes are packed LSB to MSB. So we have to reverse the strings byte by byte. Let’s also define some convenience functions to help us compute numbers
1(require [clojure.string :as str])
2
3(defn reverse-str-bytewise [s]
4 (->> (str/replace s " " "")
5 (partition 8)
6 (map #(apply str %))
7 (map str/reverse)))
8
9(reverse-str-bytewise "01001011 01001100 10000100 00000000 00101110 00000000")
10; ("11010010" "00110010" "00100001" "00000000" "01110100" "00000000")
11; ^^^^^^ This is the bitstream we will examine below ^^^^^^
12
13(defn str->bits [s] (->> s (str/reverse) (mapv #(if (= % \1) 1 0))))
14
15(str->bits "110010")
16; [0 1 0 0 1 1]
17
18(defn bin->dec [s]
19 (->> s
20 (str->bits)
21 (reduce-kv (fn [acc, i, elem]
22 (if (= elem 1) (+ acc (bit-shift-left 1 i)) acc))
23 0))
24
25(bin->dec "10001")
26; 17
The above is Clojure code. Don’t worry if you don’t understand it, it’s just some helpers
Decoding the block
8bitswise: 11010010 00110010 00100001 00000000 01110100 00000000
separated: 1 10 10010001 10010001 0000100 00000 00111010 0000000 00
1 - final block
01 - compressed with fixed huffman codes (don’t forget that although the bitstream says “10”, it is read as 01 because the data literals are to be interpreted in little endian format)
Next we have to decode the huffman codes. If you haven’t read the official DEFLATE spec, the gist of fixed (aka static) huffman codes is that they are a bunch of agreed upon huffman codes that range from 7-9 bits long. As with all huffman codes, each code is prefix free, which means that as you read the bitstream bit by bit, there’s never any ambiguitity about when each code ends.
If you’re unsure, read section 3.2.5 and 3.2.6 (fixed huffman codes) of the DEFLATE spec before continuing on or it won’t make much sense. The Wikipedia page for canonical Huffman codes is also a good read.
I’ve included the huffman table from the spec below:
Lit Value Bits Codes
--------- ---- -----
0 - 143 8 00110000 through
10111111
144 - 255 9 110010000 through
111111111
256 - 279 7 0000000 through
0010111
280 - 287 8 11000000 through
11000111
To decode the huffman codes, we have to (possibly) read up to the next 9 bits. We’ll start by reading the bitstream bit by bit, until we identify a prefix that uniquely identifies which character it is. You can conceptually think of this as walking down the edges of the huffman tree.
Side note: Huffman decoders will usually just read 9 bits straight away, look them up in a table, and then “put back” whatever bits it didn’t need, instead of wasting precious CPU cycles reading bit by bit.
Our next 9 bits is 100100011
- this has the prefix 100, which tells us that is a literal between 0-143. So it is only 8 bits (1001 0001).
Don’t forget that the huffman codes are packed LSB to MSB, but are to be interpreted as an integer in big endian format. Why this insanity? ¯\(ツ)/¯ …
Decoding, we get: val = (10010001 subtract 00110000) = 145 - 48 = 97
97 is the ASCII for ‘a’. Perfect!
Decoding the rest of the bits, we get:
1 10 10010001 10010001 0000100 00000 00111010 0000000 00
97 97 260 0 58 256 -
'a' 'a' repeat 6x 1 behind 0x10 (LF) HALT <padding>
LIT LIT LEN DIST LIT
Those are our 8 ‘a’s! two literals followed by a repeat of 6 ‘a’s, then a LF.
The “repeat 6x, 1 behind” is a length+distance (LEN,DIST) code, and it tells the decoder that the character to be repeated is the previous one that it just decoded. Which is ‘a’, in this case.
Not too shabby, we’ve encoded our 8 ‘a’s and LF (originally 72 bits) into 46 bits with 2 padding bits.
But I feel that gzip could have done better, though. It could have simply encoded a single a
followed by 261, which is the code to repeat 7 times. I thought this was because I ran gzip -1
, but running regular gzip seems to repro this. I don’t understand why this is the case and I hope someone can explain this.
Finishing off - checksum and size
Let’s finish off the gzip file. Next we’re supposed to see a CRC32. Going to an online crc32 tool, we see that the uncompressed 8 ‘a’s with a line feed will generate: ad d7 66 b6
.
Indeed, if we look at the hex stream again:
$ xxd test.out.gz
00000000: 1f8b 0808 bf35 6a61 0403 7465 7374 2e6f .....5ja..test.o
00000010: 7574 004b 4c84 002e 00b6 66d7 ad09 0000 ut.KL.....f.....
00000020: 00 ^^^^^^^^^^
Here!
We can clearly see b6 66 d7 ad
, in little endian byte order. This is the crc checksum.
The next 4 bytes 09 00 00 00
is little endian byte order for 9 bytes. Indeed, we decoded 9 bytes, and there are 9 bytes in our input file.
Summary
So this is the file breakdown:
gzip info: 1f8b 0808 bf35 6a61 0403
filename: 7465 7374 2e6f 7574 00
DEFLATE data: 4b 4c 84 00 2e 00
crc32: b6 66 d7 ad
size: 09 00 00 00
Of course, this barely scratches the surface - the real meat of the gzip algorithm is the dynamic huffman codes. In the future, I want to write a part 2 covering this but it will probably be less by hand-ish, and more of an explanatory algorithm to inflate the huffman tables. I tried doing dynamic codes by hand and I quickly ran out of patience after an hour.
If you see any mistakes, please correct them on Github, or email me at thomastayac
. Google mail.
References
I found these articles extremely helpful, in no particular order:


Who I am
My Name is Samuel Nwoye, Certified Kubernetes Administrator and AWS Certified Solutions Architect helping companies bring visibility into their infrastructure, removing bottle necks from these systems and automating their software development and delivery workflows.
What I do
I weave key automation technologies together in evaluating infrastructure and workloads thereon, strip superfluous resources to save costs, advise as well as implement solutions that increases efficiency. I do these by:
- working closely with developement team to manage the software delivery of the application.
- deploying and managing seamless pipelines that enable them run their processes effeciently.
- delivering business insight that uncover key areas to save costs for their business.


Intent: Loop through a list of numbers and pick out the ones greater than 5, and multiply them by 100. Return them as a list.
#!/usr/bin/env python
# Long explicit way:
result = []
for n in range(1,8):
if n > 5:
result.append(n*100)
print result
# result [600, 700]
# Concise way:
print [n*100 for n in [1,2,3,4,5,6,7] if n > 5]
# result [600, 700]
And the ruby way...
#!/usr/bin/env ruby
# Long explicit way:
result = []
(1...8).each do |n|
if n > 5
result << n*100
end
end
p result
# result [600, 700]
# Concise way:
p [1,2,3,4,5,6,7].select{|n| n > 5}.map{|n| n*100}
# result [600, 700]
Thoughts:
It seems to me that the puzzle (such that it is) consists of two distinct stages.
The first stage is to select items from the list and the second stage is to apply a transformation onto each selected element.
I used to think that the python syntax was superior, but once you get around the idea of the two stages, the ruby syntax makes perfect sense too.
Also, I guess the “select + map idiom” is so common that the special python syntax wins in that it isn’t telling you how anything is being done – the “list comprehension” just reads better.
The ruby version is a little more cryptic to the newcomer I guess and is telling you how everything is being done.
The ruby version on the other hand is more general, and would allow other varieties of select/transform/manipulate to get plugged in, and you can have as many as you like e.g.


Image 1: {{form.image1.name}}
Image 2: {{form.image2.name}}
Duration: {{form.survey.duration}}
Sympthoms: {{form.survey.itchy}};{{form.survey.hurts}};{{form.survey.hotfeel}};{{form.survey.coldfeel}};
Description: {{form.survey.description}}
Gender: {{form.survey.sex}}
Age: {{form.survey.age}}
Email: {{form.survey.email}}


Get unique labels and indices for batched labels for tf.nn.ctc_loss
.
tf.compat.v1.nn.ctc_unique_labels
tf.compat.v2.nn.ctc_unique_labels
tf.nn.ctc_unique_labels( labels, name=None )
For use with tf.nn.ctc_loss
optional argument unique
: This op can be
used to preprocess labels in input pipeline to for better speed/memory use
computing the ctc loss on TPU.
ctc_unique_labels([[3, 4, 4, 3]]) -> unique labels padded with 0: [[3, 4, 0, 0]] indices of original labels in unique: [0, 1, 1, 0]
labels
: tensor of shape [batch_size, max_label_length] padded with 0.name
: A name for this Op
. Defaults to "ctc_unique_labels".tuple of
- unique labels, tensor of shape [batch_size, max_label_length]
- indices into unique labels, shape [batch_size, max_label_length]


| CLI for C++ Project Page | CLI for C++ Home Page |
#include <ParseException.h>
Public Member Functions | |
| ParseException () | |
| ParseException (const char *const message) |
|
|
|
|
|
|
| hosts this site. |
Send comments to: John Poplett |


Newbie Guide:
Select the one you seek below. Their location will be indicated on the radar.
Trader Lector
Armor Merchant Jackson
Back To The Main List


The janitor functions expedite the initial data exploration and cleaning that comes with any new data set. This catalog describes the usage for each function.
Functions for everyday use.
clean_names()
Call this function every time you read data.
It works in a %>%
pipeline, and handles problematic variable names, especially those that are so well-preserved by readxl::read_excel()
and readr::read_csv()
.
œ
to oe
.# Create a data.frame with dirty names
test_df <- as.data.frame(matrix(ncol = 6))
names(test_df) <- c("firstName", "ábc@!*", "% successful (2009)",
"REPEAT VALUE", "REPEAT VALUE", "")
Clean the variable names, returning a data.frame:
test_df %>%
clean_names()
#> first_name abc percent_successful_2009 repeat_value repeat_value_2 x
#> 1 NA NA NA NA NA NA
Compare to what base R produces:
make.names(names(test_df))
#> [1] "firstName" "ábc..." "X..successful..2009."
#> [4] "REPEAT.VALUE" "REPEAT.VALUE" "X"
This function is now powered by the underlying exported function make_clean_names()
, which accepts and returns a character vector of names (see below). This allows for cleaning the names of any object, not just a data.frame. clean_names()
is retained for its convenience in piped workflows, and can be called on an sf
simple features object in addition to a data.frame.
compare_df_cols()
For cases when you are given a set of data files that should be identical, and you wish to read and combine them for analysis. But then dplyr::bind_rows()
or rbind()
fails, because of different columns or because the column classes don’t match across data.frames.
compare_df_cols()
takes unquoted names of data.frames / tibbles, or a list of data.frames, and returns a summary of how they compare. See what the column types are, which are missing or present in the different inputs, and how column types differ.
df1 <- data.frame(a = 1:2, b = c("big", "small")) # a factor by default
df2 <- data.frame(a = 10:12, b = c("medium", "small", "big"), c = 0, stringsAsFactors = FALSE)
df3 <- df1 %>%
dplyr::mutate(b = as.character(b))
compare_df_cols(df1, df2, df3)
#> column_name df1 df2 df3
#> 1 a integer integer integer
#> 2 b factor character character
#> 3 c <NA> numeric <NA>
compare_df_cols(df1, df2, df3, return = "mismatch")
#> column_name df1 df2 df3
#> 1 b factor character character
compare_df_cols(df1, df2, df3, return = "mismatch", bind_method = "rbind") # default is dplyr::bind_rows
#> column_name df1 df2 df3
#> 1 b factor character character
#> 2 c <NA> numeric <NA>
compare_df_cols_same()
returns TRUE
or FALSE
indicating if the data.frames can be successfully row-bound with the given binding method:
compare_df_cols_same(df1, df3)
#> column_name ..1 ..2
#> 1 b factor character
#> [1] FALSE
compare_df_cols_same(df2, df3)
#> [1] TRUE
tabyl()
- a better version of table()
tabyl()
is a tidyverse-oriented replacement for table()
. It counts combinations of one, two, or three variables, and then can be formatted with a suite of adorn_*
functions to look just how you want. For instance:
mtcars %>%
tabyl(gear, cyl) %>%
adorn_totals("col") %>%
adorn_percentages("row") %>%
adorn_pct_formatting(digits = 2) %>%
adorn_ns() %>%
adorn_title()
#> cyl
#> gear 4 6 8 Total
#> 3 6.67% (1) 13.33% (2) 80.00% (12) 100.00% (15)
#> 4 66.67% (8) 33.33% (4) 0.00% (0) 100.00% (12)
#> 5 40.00% (2) 20.00% (1) 40.00% (2) 100.00% (5)
Learn more in the tabyls vignette.
get_dupes()
This is for hunting down and examining duplicate records during data cleaning - usually when there shouldn’t be any.
For example, in a tidy data.frame you might expect to have a unique ID repeated for each year, but no duplicated pairs of unique ID & year. Say you want to check for and study any such duplicated records.
get_dupes()
returns the records (and inserts a count of duplicates) so you can examine the problematic cases:
get_dupes(mtcars, wt, cyl) # or mtcars %>% get_dupes(wt, cyl) if you prefer to pipe
#> # A tibble: 4 x 12
#> wt cyl dupe_count mpg disp hp drat qsec vs am gear
#> <dbl> <dbl> <int> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl>
#> 1 3.44 6 2 19.2 168. 123 3.92 18.3 1 0 4
#> 2 3.44 6 2 17.8 168. 123 3.92 18.9 1 0 4
#> 3 3.57 8 2 14.3 360 245 3.21 15.8 0 0 3
#> 4 3.57 8 2 15 301 335 3.54 14.6 0 1 5
#> # … with 1 more variable: carb <dbl>
Smaller functions for use in particular situations. More human-readable than the equivalent code they replace.
make_clean_names()
Like base R’s make.names()
, but with the stylings and case choice of the long-time janitor function clean_names()
. While clean_names()
is still offered for use in data.frame pipeline with %>%
, make_clean_names()
allows for more general usage, e.g., on a vector.
It can also be used as an argument to .name_repair
in the newest version of tibble::as_tibble
:
tibble::as_tibble(iris, .name_repair = janitor::make_clean_names)
#> # A tibble: 150 x 5
#> sepal_length sepal_width petal_length petal_width species
#> <dbl> <dbl> <dbl> <dbl> <fct>
#> 1 5.1 3.5 1.4 0.2 setosa
#> 2 4.9 3 1.4 0.2 setosa
#> 3 4.7 3.2 1.3 0.2 setosa
#> 4 4.6 3.1 1.5 0.2 setosa
#> 5 5 3.6 1.4 0.2 setosa
#> 6 5.4 3.9 1.7 0.4 setosa
#> 7 4.6 3.4 1.4 0.3 setosa
#> 8 5 3.4 1.5 0.2 setosa
#> 9 4.4 2.9 1.4 0.2 setosa
#> 10 4.9 3.1 1.5 0.1 setosa
#> # … with 140 more rows
remove_empty()
rows and columnsDoes what it says. For cases like cleaning Excel files that contain empty rows and columns after being read into R.
q <- data.frame(v1 = c(1, NA, 3),
v2 = c(NA, NA, NA),
v3 = c("a", NA, "b"))
q %>%
remove_empty(c("rows", "cols"))
#> v1 v3
#> 1 1 a
#> 3 3 b
Just a simple wrapper for one-line functions, but it saves a little thinking for both the code writer and the reader.
remove_constant()
columnsDrops columns from a data.frame that contain only a single constant value (with an na.rm
option to control whether NAs should be considered as different values from the constant).
remove_constant
and remove_empty
work on matrices as well as data.frames.
a <- data.frame(good = 1:3, boring = "the same")
a %>% remove_constant()
#> good
#> 1 1
#> 2 2
#> 3 3
round_half_up()
R uses “banker’s rounding”, i.e., halves are rounded to the nearest even number. This function, an exact implementation of https://stackoverflow.com/questions/12688717/round-up-from-5/12688836#12688836, will round all halves up. Compare:
nums <- c(2.5, 3.5)
round(nums)
#> [1] 2 4
round_half_up(nums)
#> [1] 3 4
round_to_fraction()
Say your data should only have values of quarters: 0, 0.25, 0.5, 0.75, 1, etc. But there are either user-entered bad values like 0.2
or floating-point precision problems like 0.25000000001
. round_to_fraction()
will enforce the desired fractional distribution by rounding the values to the nearest value given the specified denominator.
There’s also a digits
argument for optional subsequent rounding.
excel_numeric_to_date()
Ever load data from Excel and see a value like 42223
where a date should be? This function converts those serial numbers to class Date
, with options for different Excel date encoding systems, preserving fractions of a date as time (in which case the returned value is of class POSIXlt
), and specifying a time zone.
excel_numeric_to_date(41103)
#> [1] "2012-07-13"
excel_numeric_to_date(41103.01) # ignores decimal places, returns Date object
#> [1] "2012-07-13"
excel_numeric_to_date(41103.01, include_time = TRUE) # returns POSIXlt object
#> [1] "2012-07-13 01:14:24 EDT"
excel_numeric_to_date(41103.01, date_system = "mac pre-2011")
#> [1] "2016-07-14"
If a data.frame has the intended variable names stored in one of its rows, row_to_names
will elevate the specified row to become the names of the data.frame and optionally (by default) remove the row in which names were stored and/or the rows above it.
dirt <- data.frame(X_1 = c(NA, "ID", 1:3),
X_2 = c(NA, "Value", 4:6))
row_to_names(dirt, 2)
#> ID Value
#> 3 1 4
#> 4 2 5
#> 5 3 6
top_levels()
Originally designed for use with Likert survey data stored as factors. Returns a tbl_df
frequency table with appropriately-named rows, grouped into head/middle/tail groups.
NA
values.f <- factor(c("strongly agree", "agree", "neutral", "neutral", "disagree", "strongly agree"),
levels = c("strongly agree", "agree", "neutral", "disagree", "strongly disagree"))
top_levels(f)
#> f n percent
#> strongly agree, agree 3 0.5000000
#> neutral 2 0.3333333
#> disagree, strongly disagree 1 0.1666667
top_levels(f, n = 1)
#> f n percent
#> strongly agree 2 0.3333333
#> agree, neutral, disagree 4 0.6666667
#> strongly disagree 0 0.0000000


The Github repo for this demo is here.
tl;dr error messages need smarter search, social analytics, and a move to real machine learning. This is an interactive demo imaging a better experience for error messages. I hope it will spark discussion, spur some interest, and perhaps lead to some great new tools.
To view the demo open the javascript console in your browser by pressing
Option-Command-J
. Please experience it in Chrome or Firefox.
(I'm showing this off in the browser so that you can play along in the console, but these same principles could be
extended to any IDE or terminal session. At the end of this article I show some exploratory work I've done in
those areas.)
Let's begin with a small improvement to simply connect errors to the web. Just give the user a link to the official docs for the error. Better yet, link to the docs in the user's language.
It's an open secret that the first thing you do when you get an error is to search for it. So why are developers manually selecting text and copy-pasting it into Google? We know that 90% of the time we're going to end up on Stack Overflow, so let's automatically search there.
Any developer knows when to strip out details that are specific to their code. Our system should be smart enough to do this too. Of course, there are times when these little details are important, for example when the error happens in a specific line of a dependency. So the right thing to do is to search for both the exact text and for a "generic" version.
Our smarter error handler should also have the smarts to know what a good result is. A "good" result is one that is recent, that contains a substantial match to the query.
Not yet implemented :(
Sometimes the error you're having is a known issue with your repo. Shouldn't you be notified of that right away? So let's also search the repo issues and wiki!
And while we're at it: If there is no open issue for our error, let's provide a quick link to create one!
Any good developer knows that sometimes you need to "massage" your error search query with details about your environment. The first plan of attack here is to try adding dependency names to your search queries. So if your project uses react, you might add "react" to the query. Rather than try to guess which dependency to add, why not just try all of them?
Doing this in the demo would exhaust our quota for Stack Overflow, so we'll just link to the searches. (But in a perfect solution, all of these searches would happen in the background and you'd be alerted to any good looking results.)
Run this code, and then select the "React" search from the dependencies.
All this searching is great, but sometimes the problem isn't in our repo at all, but in one of our dependencies.
So let's search issues for our dependencies.
...not yet implemented...
All that automatic searching is great, but sometimes the solution doesn't live in a web result
Who of my co-workers has seen this error lately? Errors are often very specific to the codebase. Knowledge of “How to fix error X?” often lives only in the head of someone else. So when you see an error, you should instantly see who of co-workers has seen this error lately.
How often is this error happening? My engineering team once lost an entire mornign trying to fix a "bug" that turned out to be very subtly caused by an overnight update in an npm package. Little did we know that that literally thousads of other developers aroudn the world were getting the same error that morning! We should have seen immediately that this error message has spiked in the last 24 hours.
While this shows off error messagse in a browser, the same concepts apply in any programming envitonment. My friend Gabe and I also built similar functionality as a plugin for VS Code, and directly in the terminal (bash).
Its an open secret these days that software development consists of a lot of "run code", "get error", "copy-paste-error to google", "open top Stack Overflow link".
Good developers know meta-search skills like removing code-specific function and variable names or recognizing when the results are too general. They know when to add a dependency to the search, or perhaps a bit of the stack trace.
Experienced developers know when they are out of options, and will "escalate" to asking co-workers/co-contributers on Slack. "Has anyone seen this error before?"
This repot is a "concept car" of what error messages could be. We hope it will spur interest and perhaps this can become a real tool someday. This works for javascript errors in Chrome or Firefox, but these ideas could be extended to anywhere errors appear
We have also built basic versions of this functionality as a plugin for VS Code, and directly in the terminal (bash).
- Deeper description of error from official docs - Ideal world there'd be some standard so that any package (e.g. react) could publish error info. But for now we do it by hand. - Where possible, error description in developers native language (esp important for beginnerin developers) - '"0"-prefixed octal literals and octal escape seq. are deprecated' is a tough English sentance for a non-native speaker.
- Search google. 80% of the time land on stackoverflow - Including long-shot searches that a coder would only do - Remove code-specific parts of error that would be irrelevant, e.g. "myFunnyVariableName" - Search open issues for the project - If some other dev has reported the problem, know it immediately. - Search dependencies - Use stack trace to infer what dependency might be culpre - Serach open issues for dependencies - Extend serach to issues for dependencies, even going down entire dependency textarea - Search with stack trace - OVERALL: know what a "good" result looks like. - A match with stack trace is better than one just for general type of error
- Record errors and context. - Has this error been spiking worldwide in the last 24 hours? (Maybe a dependency had a botched upgrade.) - Basic analytics on error freqency and first occurance. (a la Sentry) - Who of my co-workers/co-contributors has seen this error? - Especially if with same/similar stack trace / branch
- We can also go beyond automating developer behavior. - Instead of naive text matching search, use the full context of an error when searching for relevant answers, issues, people. - Repo / branch - Stack trace - Dependencies - Input/launch configurations - Use the logging info to record the total context of an error. Then when the error happens later for someone else, they can know who to talk to, or even automatically see what worked to fix it.